diff --git a/api/main.go b/api/main.go index c972200a..d5107f3f 100644 --- a/api/main.go +++ b/api/main.go @@ -7,12 +7,10 @@ import ( "net/http" "os" "strconv" - "time" "github.com/ansrivas/fiberprometheus/v2" "github.com/gofiber/adaptor/v2" "github.com/gofiber/fiber/v2" - "github.com/gofiber/fiber/v2/middleware/cache" "github.com/gofiber/fiber/v2/middleware/cors" "github.com/gofiber/fiber/v2/middleware/logger" "github.com/gofiber/fiber/v2/middleware/requestid" @@ -20,7 +18,6 @@ import ( ipfslog "github.com/ipfs/go-log/v2" "github.com/wormhole-foundation/wormhole-explorer/api/handlers/governor" - "github.com/wormhole-foundation/wormhole-explorer/api/handlers/guardian" "github.com/wormhole-foundation/wormhole-explorer/api/handlers/heartbeats" "github.com/wormhole-foundation/wormhole-explorer/api/handlers/infrastructure" "github.com/wormhole-foundation/wormhole-explorer/api/handlers/observations" @@ -30,24 +27,12 @@ import ( "github.com/wormhole-foundation/wormhole-explorer/api/internal/db" "github.com/wormhole-foundation/wormhole-explorer/api/middleware" "github.com/wormhole-foundation/wormhole-explorer/api/response" + "github.com/wormhole-foundation/wormhole-explorer/api/routes/guardian" + "github.com/wormhole-foundation/wormhole-explorer/api/routes/wormscan" rpcApi "github.com/wormhole-foundation/wormhole-explorer/api/rpc" "go.uber.org/zap" ) -var cacheConfig = cache.Config{ - Next: func(c *fiber.Ctx) bool { - return c.Query("refresh") == "true" - }, - Expiration: 1 * time.Second, - CacheControl: true, - StoreResponseHeaders: true, -} - -func healthOk(ctx *fiber.Ctx) error { - ctx.Status(200) - return ctx.SendString("Ok") -} - //go:embed docs/swagger.json var swagger []byte @@ -116,111 +101,40 @@ func main() { // Get cache get function cacheGetFunc := NewCache(cfg, rootLogger) - // Setup repositories + // Set up repositories vaaRepo := vaa.NewRepository(db, rootLogger) obsRepo := observations.NewRepository(db, rootLogger) governorRepo := governor.NewRepository(db, rootLogger) infrastructureRepo := infrastructure.NewRepository(db, rootLogger) heartbeatsRepo := heartbeats.NewRepository(db, rootLogger) - // Setup services + // Set up services vaaService := vaa.NewService(vaaRepo, cacheGetFunc, rootLogger) obsService := observations.NewService(obsRepo, rootLogger) governorService := governor.NewService(governorRepo, rootLogger) infrastructureService := infrastructure.NewService(infrastructureRepo, rootLogger) heartbeatsService := heartbeats.NewService(heartbeatsRepo, rootLogger) - // Setup controllers - vaaCtrl := vaa.NewController(vaaService, rootLogger) - observationsCtrl := observations.NewController(obsService, rootLogger) - governorCtrl := governor.NewController(governorService, rootLogger) - infrastructureCtrl := infrastructure.NewController(infrastructureService) - guardianCtrl := guardian.NewController(rootLogger) - heartbeatsCtrl := heartbeats.NewController(heartbeatsService, rootLogger) - - // Setup app with custom error handling. + // Set up a custom error handler response.SetEnableStackTrace(*cfg) app := fiber.New(fiber.Config{ErrorHandler: middleware.ErrorHandler}) - // Middleware + // Configure middleware prometheus := fiberprometheus.New("wormscan") prometheus.RegisterAt(app, "/metrics") app.Use(prometheus.Middleware) - app.Use(cors.New()) app.Use(requestid.New()) app.Use(logger.New(logger.Config{ Format: "level=info timestamp=${time} method=${method} path=${path} status${status} request_id=${locals:requestid}\n", })) + // Set up route handlers app.Get("/swagger.json", GetSwagger) + wormscan.RegisterRoutes(app, rootLogger, vaaService, obsService, governorService, infrastructureService) + guardian.RegisterRoutes(app, rootLogger, vaaService, governorService, heartbeatsService) - api := app.Group("/api/v1") - api.Use(middleware.ExtractPagination) - api.Get("/health", infrastructureCtrl.HealthCheck) - api.Get("/ready", infrastructureCtrl.ReadyCheck) - - // vaas resource - vaas := api.Group("/vaas") - vaas.Use(cache.New(cacheConfig)) - vaas.Get("/vaa-counts", vaaCtrl.GetVaaCount) - vaas.Get("/", vaaCtrl.FindAll) - vaas.Get("/:chain", vaaCtrl.FindByChain) - vaas.Get("/:chain/:emitter", vaaCtrl.FindByEmitter) - vaas.Get("/:chain/:emitter/:sequence", vaaCtrl.FindById) - - // oservations resource - observations := api.Group("/observations") - observations.Get("/", observationsCtrl.FindAll) - observations.Get("/:chain", observationsCtrl.FindAllByChain) - observations.Get("/:chain/:emitter", observationsCtrl.FindAllByEmitter) - observations.Get("/:chain/:emitter/:sequence", observationsCtrl.FindAllByVAA) - observations.Get("/:chain/:emitter/:sequence/:signer/:hash", observationsCtrl.FindOne) - - // governor resources - governor := api.Group("/governor") - governorLimit := governor.Group("/limit") - governorLimit.Get("/", governorCtrl.GetGovernorLimit) - - governorConfigs := governor.Group("/config") - governorConfigs.Get("/", governorCtrl.FindGovernorConfigurations) - governorConfigs.Get("/:guardian_address", governorCtrl.FindGovernorConfigurationByGuardianAddress) - - governorStatus := governor.Group("/status") - governorStatus.Get("/", governorCtrl.FindGovernorStatus) - governorStatus.Get("/:guardian_address", governorCtrl.FindGovernorStatusByGuardianAddress) - - governorNotional := governor.Group("/notional") - governorNotional.Get("/limit/", governorCtrl.FindNotionalLimit) - governorNotional.Get("/limit/:chain", governorCtrl.GetNotionalLimitByChainID) - governorNotional.Get("/available/", governorCtrl.GetAvailableNotional) - governorNotional.Get("/available/:chain", governorCtrl.GetAvailableNotionalByChainID) - governorNotional.Get("/max_available/:chain", governorCtrl.GetMaxNotionalAvailableByChainID) - - enqueueVaas := governor.Group("/enqueued_vaas") - enqueueVaas.Get("/", governorCtrl.GetEnqueueVaas) - enqueueVaas.Get("/:chain", governorCtrl.GetEnqueuedVaasByChainID) - - // v1 guardian public api. - publicAPIV1 := app.Group("/v1") - // signedVAA resource. - signedVAA := publicAPIV1.Group("/signed_vaa") - signedVAA.Get("/:chain/:emitter/:sequence", vaaCtrl.FindSignedVAAByID) - signedBatchVAA := publicAPIV1.Group("/signed_batch_vaa") - signedBatchVAA.Get("/:chain/:trxID/:nonce", vaaCtrl.FindSignedBatchVAAByID) - // guardianSet resource. - guardianSet := publicAPIV1.Group("/guardianset") - guardianSet.Get("/current", guardianCtrl.GetGuardianSet) - // heartbeats resource. - heartbeats := publicAPIV1.Group("/heartbeats") - heartbeats.Get("", heartbeatsCtrl.GetLastHeartbeats) - // governor resource. - gov := publicAPIV1.Group("/governor") - gov.Get("/available_notional_by_chain", governorCtrl.GetAvailNotionByChain) - gov.Get("/enqueued_vaas", governorCtrl.GetEnqueuedVaas) - gov.Get("/is_vaa_enqueued/:chain/:emitter/:sequence", governorCtrl.IsVaaEnqueued) - gov.Get("/token_list", governorCtrl.GetTokenList) - + // Set up gRPC handlers handler := rpcApi.NewHandler(vaaService, heartbeatsService, governorService, rootLogger) grpcServer := rpcApi.NewServer(handler, rootLogger) grpcWebServer := grpcweb.WrapServer(grpcServer) diff --git a/api/routes/guardian/governor/controller.go b/api/routes/guardian/governor/controller.go new file mode 100644 index 00000000..d9209a90 --- /dev/null +++ b/api/routes/guardian/governor/controller.go @@ -0,0 +1,181 @@ +// Package governor handle the request of governor data from governor endpoint defined in the api. +package governor + +import ( + "strconv" + + "github.com/gofiber/fiber/v2" + "github.com/wormhole-foundation/wormhole-explorer/api/handlers/governor" + "github.com/wormhole-foundation/wormhole-explorer/api/middleware" + _ "github.com/wormhole-foundation/wormhole-explorer/api/response" // needed by swaggo docs + "github.com/wormhole-foundation/wormhole/sdk/vaa" + "go.uber.org/zap" +) + +// Controller definition. +type Controller struct { + srv *governor.Service + logger *zap.Logger +} + +// NewController create a new controler. +func NewController(serv *governor.Service, logger *zap.Logger) *Controller { + return &Controller{srv: serv, logger: logger.With(zap.String("module", "GovernorController"))} +} + +// AvailableNotionalResponse response compatible with grpc api. +type AvailableNotionalResponse struct { + Entries []*AvailableNotionalItemResponse `json:"entries"` +} + +type AvailableNotionalItemResponse struct { + ChainID vaa.ChainID `json:"chainId"` + AvailableNotional string `json:"remainingAvailableNotional"` + NotionalLimit string `json:"notionalLimit"` + MaxTransactionSize string `json:"bigTransactionSize"` +} + +// GetAvailNotionByChain godoc +// @Description Get available notional by chainID +// @Description Since from the wormhole-explorer point of view it is not a node, but has the information of all nodes, +// @Description in order to build the endpoints it was assumed: +// @Description There are N number of remainingAvailableNotional values in the GovernorConfig collection. N = number of guardians +// @Description for a chainID. The smallest remainingAvailableNotional value for a chainID is used for the endpoint response. +// @Tags Guardian +// @ID governor-available-notional-by-chain +// @Success 200 {object} AvailableNotionalResponse +// @Failure 400 +// @Failure 500 +// @Router /v1/governor/available_notional_by_chain [get] +func (c *Controller) GetAvailNotionByChain(ctx *fiber.Ctx) error { + // call service to get available notional by chainID + availableNotional, err := c.srv.GetAvailNotionByChain(ctx.Context()) + if err != nil { + return err + } + + // build response compatible with node grpc api. + entries := make([]*AvailableNotionalItemResponse, 0, len(availableNotional)) + for _, v := range availableNotional { + r := AvailableNotionalItemResponse{ + ChainID: v.ChainID, + AvailableNotional: v.AvailableNotional.String(), + NotionalLimit: v.NotionalLimit.String(), + MaxTransactionSize: v.MaxTransactionSize.String(), + } + entries = append(entries, &r) + } + response := AvailableNotionalResponse{ + Entries: entries, + } + return ctx.JSON(response) +} + +// EnqueuedVaaResponse response compatible with grpc api. +type EnqueuedVaaResponse struct { + Entries []*EnqueuedVaaItemResponse `json:"entries"` +} + +type EnqueuedVaaItemResponse struct { + EmitterChain vaa.ChainID `json:"emitterChain"` + EmitterAddress string `json:"emitterAddress"` + Sequence uint64 `json:"sequence"` + ReleaseTime int64 `json:"releaseTime"` + NotionalValue string `json:"notionalValue"` + TxHash string `json:"txHash"` +} + +// GetEnqueuedVaas godoc +// @Description Get enqueued VAAs +// @Tags Guardian +// @ID guardians-enqueued-vaas +// @Success 200 {object} EnqueuedVaaResponse +// @Failure 400 +// @Failure 500 +// @Router /v1/governor/enqueued_vaas [get] +func (c *Controller) GetEnqueuedVaas(ctx *fiber.Ctx) error { + enqueuedVaa, err := c.srv.GetEnqueuedVaas(ctx.Context()) + if err != nil { + return err + } + + // build response compatible with node grpc api. + entries := make([]*EnqueuedVaaItemResponse, 0, len(enqueuedVaa)) + for _, v := range enqueuedVaa { + seqUint64, err := strconv.ParseUint(v.Sequence, 10, 64) + if err != nil { + return err + } + r := EnqueuedVaaItemResponse{ + EmitterChain: v.EmitterChain, + EmitterAddress: v.EmitterAddress, + Sequence: seqUint64, + ReleaseTime: v.ReleaseTime, + NotionalValue: v.NotionalValue.String(), + TxHash: v.TxHash, + } + entries = append(entries, &r) + } + response := EnqueuedVaaResponse{ + Entries: entries, + } + + return ctx.JSON(response) +} + +// IsVaaEnqueued godoc +// @Description Check if vaa is enqueued +// @Tags Guardian +// @ID guardians-is-vaa-enqueued +// @Param chain_id path integer true "id of the blockchain" +// @Param emitter path string true "address of the emitter" +// @Param seq path integer true "sequence of the vaa" +// @Success 200 {object} EnqueuedVaaResponse +// @Failure 400 +// @Failure 500 +// @Router /v1/governor/is_vaa_enqueued/{chain_id}/{emitter}/{seq} [get] +func (c *Controller) IsVaaEnqueued(ctx *fiber.Ctx) error { + chainID, emitter, seq, err := middleware.ExtractVAAParams(ctx, c.logger) + if err != nil { + return err + } + isEnqueued, err := c.srv.IsVaaEnqueued(ctx.Context(), chainID, *emitter, strconv.FormatUint(seq, 10)) + if err != nil { + return err + } + + // build reponse compatible with node grpc api. + response := struct { + IsEnqueued bool `json:"isEnqueued"` + }{ + IsEnqueued: isEnqueued, + } + return ctx.JSON(response) +} + +// GetTokenList godoc +// @Description Get token list +// @Description Since from the wormhole-explorer point of view it is not a node, but has the information of all nodes, +// @Description in order to build the endpoints it was assumed: +// @Description For tokens with the same originChainId and originAddress and different price values for each node, +// @Description the price that has most occurrences in all the nodes for an originChainId and originAddress is returned. +// @Tags Guardian +// @ID guardians-token-list +// @Success 200 {object} []TokenList +// @Failure 400 +// @Failure 500 +// @Router /v1/governor/token_list [get] +func (c *Controller) GetTokenList(ctx *fiber.Ctx) error { + tokenList, err := c.srv.GetTokenList(ctx.Context()) + if err != nil { + return err + } + + // build reponse compatible with node grpc api. + response := struct { + Entries []*governor.TokenList `json:"entries"` + }{ + Entries: tokenList, + } + return ctx.JSON(response) +} diff --git a/api/handlers/guardian/controller.go b/api/routes/guardian/guardian/controller.go similarity index 91% rename from api/handlers/guardian/controller.go rename to api/routes/guardian/guardian/controller.go index 3d15f13c..21fc00a4 100644 --- a/api/handlers/guardian/controller.go +++ b/api/routes/guardian/guardian/controller.go @@ -2,6 +2,7 @@ package guardian import ( "github.com/gofiber/fiber/v2" + "github.com/wormhole-foundation/wormhole-explorer/api/handlers/guardian" "github.com/wormhole-foundation/wormhole-explorer/api/response" "go.uber.org/zap" ) @@ -37,12 +38,12 @@ type GuardianSet struct { // @Router /v1/guardianset/current [get] func (c *Controller) GetGuardianSet(ctx *fiber.Ctx) error { // check guardianSet exists. - if len(ByIndex) == 0 { + if len(guardian.ByIndex) == 0 { return response.NewApiError(ctx, fiber.StatusServiceUnavailable, response.Unavailable, "guardian set not fetched from chain yet", nil) } // get lasted guardianSet. - guardinSet := GetLatest() + guardinSet := guardian.GetLatest() // get guardian addresses. addresses := make([]string, len(guardinSet.Keys)) diff --git a/api/handlers/heartbeats/controller.go b/api/routes/guardian/heartbeats/controller.go similarity index 93% rename from api/handlers/heartbeats/controller.go rename to api/routes/guardian/heartbeats/controller.go index 54dec424..3c04a38c 100644 --- a/api/handlers/heartbeats/controller.go +++ b/api/routes/guardian/heartbeats/controller.go @@ -5,18 +5,19 @@ import ( "github.com/gofiber/fiber/v2" "github.com/wormhole-foundation/wormhole-explorer/api/handlers/guardian" + "github.com/wormhole-foundation/wormhole-explorer/api/handlers/heartbeats" "github.com/wormhole-foundation/wormhole-explorer/api/response" "go.uber.org/zap" ) // Controller definition. type Controller struct { - srv *Service + srv *heartbeats.Service logger *zap.Logger } // NewController create a new controler. -func NewController(srv *Service, logger *zap.Logger) *Controller { +func NewController(srv *heartbeats.Service, logger *zap.Logger) *Controller { return &Controller{ srv: srv, logger: logger.With(zap.String("module", "HeartbeatsController")), @@ -82,7 +83,7 @@ func (c *Controller) GetLastHeartbeats(ctx *fiber.Ctx) error { return ctx.Status(fiber.StatusOK).JSON(response) } -func buildHeartbeatResponse(heartbeats []*HeartbeatDoc) *HeartbeatsResponse { +func buildHeartbeatResponse(heartbeats []*heartbeats.HeartbeatDoc) *HeartbeatsResponse { if heartbeats == nil { return nil } diff --git a/api/routes/guardian/routes.go b/api/routes/guardian/routes.go new file mode 100644 index 00000000..b66dfcfa --- /dev/null +++ b/api/routes/guardian/routes.go @@ -0,0 +1,53 @@ +package guardian + +import ( + "github.com/gofiber/fiber/v2" + govsvc "github.com/wormhole-foundation/wormhole-explorer/api/handlers/governor" + heartbeatssvc "github.com/wormhole-foundation/wormhole-explorer/api/handlers/heartbeats" + vaasvc "github.com/wormhole-foundation/wormhole-explorer/api/handlers/vaa" + "github.com/wormhole-foundation/wormhole-explorer/api/routes/guardian/governor" + "github.com/wormhole-foundation/wormhole-explorer/api/routes/guardian/guardian" + "github.com/wormhole-foundation/wormhole-explorer/api/routes/guardian/heartbeats" + "github.com/wormhole-foundation/wormhole-explorer/api/routes/guardian/vaa" + "go.uber.org/zap" +) + +// RegisterRoutes sets up the handlers for the Guardian API. +func RegisterRoutes( + app *fiber.App, + rootLogger *zap.Logger, + vaaService *vaasvc.Service, + governorService *govsvc.Service, + heartbeatsService *heartbeatssvc.Service, +) { + + // Set up controllers + vaaCtrl := vaa.NewController(vaaService, rootLogger) + governorCtrl := governor.NewController(governorService, rootLogger) + guardianCtrl := guardian.NewController(rootLogger) + heartbeatsCtrl := heartbeats.NewController(heartbeatsService, rootLogger) + + // Set up route handlers + apiV1 := app.Group("/v1") + + // signedVAA resource + signedVAA := apiV1.Group("/signed_vaa") + signedVAA.Get("/:chain/:emitter/:sequence", vaaCtrl.FindSignedVAAByID) + signedBatchVAA := apiV1.Group("/signed_batch_vaa") + signedBatchVAA.Get("/:chain/:trxID/:nonce", vaaCtrl.FindSignedBatchVAAByID) + + // guardianSet resource + guardianSet := apiV1.Group("/guardianset") + guardianSet.Get("/current", guardianCtrl.GetGuardianSet) + + // heartbeats resource + heartbeats := apiV1.Group("/heartbeats") + heartbeats.Get("", heartbeatsCtrl.GetLastHeartbeats) + + // governor resource + gov := apiV1.Group("/governor") + gov.Get("/available_notional_by_chain", governorCtrl.GetAvailNotionByChain) + gov.Get("/enqueued_vaas", governorCtrl.GetEnqueuedVaas) + gov.Get("/is_vaa_enqueued/:chain/:emitter/:sequence", governorCtrl.IsVaaEnqueued) + gov.Get("/token_list", governorCtrl.GetTokenList) +} diff --git a/api/routes/guardian/vaa/controller.go b/api/routes/guardian/vaa/controller.go new file mode 100644 index 00000000..bdb50c67 --- /dev/null +++ b/api/routes/guardian/vaa/controller.go @@ -0,0 +1,73 @@ +// Package observations handle the request of VAA data from governor endpoint defined in the api. +package vaa + +import ( + "strconv" + + "github.com/gofiber/fiber/v2" + "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" + "go.uber.org/zap" +) + +// Controller definition. +type Controller struct { + srv *vaa.Service + logger *zap.Logger +} + +// NewController create a new controler. +func NewController(serv *vaa.Service, logger *zap.Logger) *Controller { + return &Controller{srv: serv, logger: logger.With(zap.String("module", "VaaController"))} +} + +// FindSignedVAAByID godoc +// @Description get a VAA []byte from a chainID, emitter address and sequence. +// @Tags Guardian +// @ID guardians-find-signed-vaa +// @Param chain_id path integer true "id of the blockchain" +// @Param emitter path string true "address of the emitter" +// @Param seq path integer true "sequence of the VAA" +// @Success 200 {object} object{vaaBytes=[]byte} +// @Failure 400 +// @Failure 500 +// @Router /v1/signed_vaa/{chain_id}/{emitter}/{seq} [get] +func (c *Controller) FindSignedVAAByID(ctx *fiber.Ctx) error { + chainID, emitter, seq, err := middleware.ExtractVAAParams(ctx, c.logger) + if err != nil { + return err + } + + // TODO + // check chainID is not Pyth. Pyth message are not stored with the other vaa. + //if ChainIDPythNet == chainID { + // return response.NewApiError(ctx, fiber.StatusBadRequest, response.InvalidParam, + // "not supported for PythNet", nil) + //} + vaa, err := c.srv.FindById(ctx.Context(), chainID, *emitter, strconv.FormatUint(seq, 10)) + if err != nil { + return err + } + response := struct { + VaaBytes []byte `json:"vaaBytes"` + }{ + VaaBytes: vaa.Data.Vaa, + } + return ctx.JSON(response) +} + +// FindSignedBatchVAAByID godoc +// @Description get a batch of VAA []byte from a chainID, emitter address and sequence. +// @Tags Guardian +// @ID guardians-find-signed-batch-vaa +// @Param chain_id path integer true "id of the blockchain" +// @Param emitter path string true "address of the emitter" +// @Param seq path integer true "sequence of the VAA" +// @Success 200 {object} object{vaaBytes=[]byte} +// @Failure 400 +// @Failure 500 +// @Router /v1/signed_batch_vaa/{chain_id}/{emitter}/sequence/{seq} [get] +func (c *Controller) FindSignedBatchVAAByID(ctx *fiber.Ctx) error { + return response.NewApiError(ctx, fiber.StatusNotImplemented, response.Unimplemented, "not yet implemented", nil) +} diff --git a/api/handlers/governor/controller.go b/api/routes/wormscan/governor/controller.go similarity index 66% rename from api/handlers/governor/controller.go rename to api/routes/wormscan/governor/controller.go index 988d679e..e00c57ee 100644 --- a/api/handlers/governor/controller.go +++ b/api/routes/wormscan/governor/controller.go @@ -2,23 +2,21 @@ package governor import ( - "strconv" - "github.com/gofiber/fiber/v2" + "github.com/wormhole-foundation/wormhole-explorer/api/handlers/governor" "github.com/wormhole-foundation/wormhole-explorer/api/middleware" _ "github.com/wormhole-foundation/wormhole-explorer/api/response" // needed by swaggo docs - "github.com/wormhole-foundation/wormhole/sdk/vaa" "go.uber.org/zap" ) // Controller definition. type Controller struct { - srv *Service + srv *governor.Service logger *zap.Logger } // NewController create a new controler. -func NewController(serv *Service, logger *zap.Logger) *Controller { +func NewController(serv *governor.Service, logger *zap.Logger) *Controller { return &Controller{srv: serv, logger: logger.With(zap.String("module", "GovernorController"))} } @@ -285,160 +283,3 @@ func (c *Controller) GetEnqueuedVaasByChainID(ctx *fiber.Ctx) error { } return ctx.JSON(enqueuedVaas) } - -// AvailableNotionalResponse response compatible with grpc api. -type AvailableNotionalResponse struct { - Entries []*AvailableNotionalItemResponse `json:"entries"` -} - -type AvailableNotionalItemResponse struct { - ChainID vaa.ChainID `json:"chainId"` - AvailableNotional string `json:"remainingAvailableNotional"` - NotionalLimit string `json:"notionalLimit"` - MaxTransactionSize string `json:"bigTransactionSize"` -} - -// GetAvailNotionByChain godoc -// @Description Get available notional by chainID -// @Description Since from the wormhole-explorer point of view it is not a node, but has the information of all nodes, -// @Description in order to build the endpoints it was assumed: -// @Description There are N number of remainingAvailableNotional values in the GovernorConfig collection. N = number of guardians -// @Description for a chainID. The smallest remainingAvailableNotional value for a chainID is used for the endpoint response. -// @Tags Guardian -// @ID governor-available-notional-by-chain -// @Success 200 {object} AvailableNotionalResponse -// @Failure 400 -// @Failure 500 -// @Router /v1/governor/available_notional_by_chain [get] -func (c *Controller) GetAvailNotionByChain(ctx *fiber.Ctx) error { - // call service to get available notional by chainID - availableNotional, err := c.srv.GetAvailNotionByChain(ctx.Context()) - if err != nil { - return err - } - - // build response compatible with node grpc api. - entries := make([]*AvailableNotionalItemResponse, 0, len(availableNotional)) - for _, v := range availableNotional { - r := AvailableNotionalItemResponse{ - ChainID: v.ChainID, - AvailableNotional: v.AvailableNotional.String(), - NotionalLimit: v.NotionalLimit.String(), - MaxTransactionSize: v.MaxTransactionSize.String(), - } - entries = append(entries, &r) - } - response := AvailableNotionalResponse{ - Entries: entries, - } - return ctx.JSON(response) -} - -// AvailableNotionalResponse response compatible with grpc api. -type EnqueuedVaaResponse struct { - Entries []*EnqueuedVaaItemResponse `json:"entries"` -} - -type EnqueuedVaaItemResponse struct { - EmitterChain vaa.ChainID `json:"emitterChain"` - EmitterAddress string `json:"emitterAddress"` - Sequence uint64 `json:"sequence"` - ReleaseTime int64 `json:"releaseTime"` - NotionalValue string `json:"notionalValue"` - TxHash string `json:"txHash"` -} - -// GetEnqueuedVaas godoc -// @Description Get enqueued VAAs -// @Tags Guardian -// @ID guardians-enqueued-vaas -// @Success 200 {object} EnqueuedVaaResponse -// @Failure 400 -// @Failure 500 -// @Router /v1/governor/enqueued_vaas [get] -func (c *Controller) GetEnqueuedVaas(ctx *fiber.Ctx) error { - enqueuedVaa, err := c.srv.GetEnqueuedVaas(ctx.Context()) - if err != nil { - return err - } - - // build response compatible with node grpc api. - entries := make([]*EnqueuedVaaItemResponse, 0, len(enqueuedVaa)) - for _, v := range enqueuedVaa { - seqUint64, err := strconv.ParseUint(v.Sequence, 10, 64) - if err != nil { - return err - } - r := EnqueuedVaaItemResponse{ - EmitterChain: v.EmitterChain, - EmitterAddress: v.EmitterAddress, - Sequence: seqUint64, - ReleaseTime: v.ReleaseTime, - NotionalValue: v.NotionalValue.String(), - TxHash: v.TxHash, - } - entries = append(entries, &r) - } - response := EnqueuedVaaResponse{ - Entries: entries, - } - - return ctx.JSON(response) -} - -// IsVaaEnqueued godoc -// @Description Check if vaa is enqueued -// @Tags Guardian -// @ID guardians-is-vaa-enqueued -// @Param chain_id path integer true "id of the blockchain" -// @Param emitter path string true "address of the emitter" -// @Param seq path integer true "sequence of the vaa" -// @Success 200 {object} EnqueuedVaaResponse -// @Failure 400 -// @Failure 500 -// @Router /v1/governor/is_vaa_enqueued/{chain_id}/{emitter}/{seq} [get] -func (c *Controller) IsVaaEnqueued(ctx *fiber.Ctx) error { - chainID, emitter, seq, err := middleware.ExtractVAAParams(ctx, c.logger) - if err != nil { - return err - } - isEnqueued, err := c.srv.IsVaaEnqueued(ctx.Context(), chainID, *emitter, strconv.FormatUint(seq, 10)) - if err != nil { - return err - } - - // build reponse compatible with node grpc api. - response := struct { - IsEnqueued bool `json:"isEnqueued"` - }{ - IsEnqueued: isEnqueued, - } - return ctx.JSON(response) -} - -// GetTokenList godoc -// @Description Get token list -// @Description Since from the wormhole-explorer point of view it is not a node, but has the information of all nodes, -// @Description in order to build the endpoints it was assumed: -// @Description For tokens with the same originChainId and originAddress and different price values for each node, -// @Description the price that has most occurrences in all the nodes for an originChainId and originAddress is returned. -// @Tags Guardian -// @ID guardians-token-list -// @Success 200 {object} []TokenList -// @Failure 400 -// @Failure 500 -// @Router /v1/governor/token_list [get] -func (c *Controller) GetTokenList(ctx *fiber.Ctx) error { - tokenList, err := c.srv.GetTokenList(ctx.Context()) - if err != nil { - return err - } - - // build reponse compatible with node grpc api. - response := struct { - Entries []*TokenList `json:"entries"` - }{ - Entries: tokenList, - } - return ctx.JSON(response) -} diff --git a/api/handlers/infrastructure/controller.go b/api/routes/wormscan/infrastructure/controller.go similarity index 83% rename from api/handlers/infrastructure/controller.go rename to api/routes/wormscan/infrastructure/controller.go index 2803e643..f74b310f 100644 --- a/api/handlers/infrastructure/controller.go +++ b/api/routes/wormscan/infrastructure/controller.go @@ -1,14 +1,17 @@ package infrastructure -import "github.com/gofiber/fiber/v2" +import ( + "github.com/gofiber/fiber/v2" + "github.com/wormhole-foundation/wormhole-explorer/api/handlers/infrastructure" +) // Controller definition. type Controller struct { - srv *Service + srv *infrastructure.Service } // NewController creates a Controller instance. -func NewController(serv *Service) *Controller { +func NewController(serv *infrastructure.Service) *Controller { return &Controller{srv: serv} } diff --git a/api/handlers/observations/controller.go b/api/routes/wormscan/observations/controller.go similarity index 96% rename from api/handlers/observations/controller.go rename to api/routes/wormscan/observations/controller.go index 04564d74..ab56913a 100644 --- a/api/handlers/observations/controller.go +++ b/api/routes/wormscan/observations/controller.go @@ -5,18 +5,19 @@ import ( "strconv" "github.com/gofiber/fiber/v2" + "github.com/wormhole-foundation/wormhole-explorer/api/handlers/observations" "github.com/wormhole-foundation/wormhole-explorer/api/middleware" "go.uber.org/zap" ) // Controller definition. type Controller struct { - srv *Service + srv *observations.Service logger *zap.Logger } // NewController create a new controler. -func NewController(srv *Service, logger *zap.Logger) *Controller { +func NewController(srv *observations.Service, logger *zap.Logger) *Controller { return &Controller{ srv: srv, logger: logger.With(zap.String("module", "ObservationsController")), diff --git a/api/routes/wormscan/routes.go b/api/routes/wormscan/routes.go new file mode 100644 index 00000000..d2707fc7 --- /dev/null +++ b/api/routes/wormscan/routes.go @@ -0,0 +1,95 @@ +package wormscan + +import ( + "time" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/cache" + "github.com/gofiber/fiber/v2/middleware/cors" + govsvc "github.com/wormhole-foundation/wormhole-explorer/api/handlers/governor" + infrasvc "github.com/wormhole-foundation/wormhole-explorer/api/handlers/infrastructure" + obssvc "github.com/wormhole-foundation/wormhole-explorer/api/handlers/observations" + vaasvc "github.com/wormhole-foundation/wormhole-explorer/api/handlers/vaa" + "github.com/wormhole-foundation/wormhole-explorer/api/middleware" + "github.com/wormhole-foundation/wormhole-explorer/api/routes/wormscan/governor" + "github.com/wormhole-foundation/wormhole-explorer/api/routes/wormscan/infrastructure" + "github.com/wormhole-foundation/wormhole-explorer/api/routes/wormscan/observations" + "github.com/wormhole-foundation/wormhole-explorer/api/routes/wormscan/vaa" + "go.uber.org/zap" +) + +var cacheConfig = cache.Config{ + Next: func(c *fiber.Ctx) bool { + return c.Query("refresh") == "true" + }, + Expiration: 1 * time.Second, + CacheControl: true, + StoreResponseHeaders: true, +} + +// RegisterRoutes sets up the handlers for the Wormscan API. +func RegisterRoutes( + app *fiber.App, + rootLogger *zap.Logger, + vaaService *vaasvc.Service, + obsService *obssvc.Service, + governorService *govsvc.Service, + infrastructureService *infrasvc.Service, +) { + + // Set up controllers + vaaCtrl := vaa.NewController(vaaService, rootLogger) + observationsCtrl := observations.NewController(obsService, rootLogger) + governorCtrl := governor.NewController(governorService, rootLogger) + infrastructureCtrl := infrastructure.NewController(infrastructureService) + + // Set up route handlers + api := app.Group("/api/v1") + api.Use(cors.New()) // TODO CORS restrictions? + api.Use(middleware.ExtractPagination) + + // monitoring + api.Get("/health", infrastructureCtrl.HealthCheck) + api.Get("/ready", infrastructureCtrl.ReadyCheck) + + // vaas resource + vaas := api.Group("/vaas") + vaas.Use(cache.New(cacheConfig)) + vaas.Get("/vaa-counts", vaaCtrl.GetVaaCount) + vaas.Get("/", vaaCtrl.FindAll) + vaas.Get("/:chain", vaaCtrl.FindByChain) + vaas.Get("/:chain/:emitter", vaaCtrl.FindByEmitter) + vaas.Get("/:chain/:emitter/:sequence", vaaCtrl.FindById) + + // oservations resource + observations := api.Group("/observations") + observations.Get("/", observationsCtrl.FindAll) + observations.Get("/:chain", observationsCtrl.FindAllByChain) + observations.Get("/:chain/:emitter", observationsCtrl.FindAllByEmitter) + observations.Get("/:chain/:emitter/:sequence", observationsCtrl.FindAllByVAA) + observations.Get("/:chain/:emitter/:sequence/:signer/:hash", observationsCtrl.FindOne) + + // governor resources + governor := api.Group("/governor") + governorLimit := governor.Group("/limit") + governorLimit.Get("/", governorCtrl.GetGovernorLimit) + + governorConfigs := governor.Group("/config") + governorConfigs.Get("/", governorCtrl.FindGovernorConfigurations) + governorConfigs.Get("/:guardian_address", governorCtrl.FindGovernorConfigurationByGuardianAddress) + + governorStatus := governor.Group("/status") + governorStatus.Get("/", governorCtrl.FindGovernorStatus) + governorStatus.Get("/:guardian_address", governorCtrl.FindGovernorStatusByGuardianAddress) + + governorNotional := governor.Group("/notional") + governorNotional.Get("/limit/", governorCtrl.FindNotionalLimit) + governorNotional.Get("/limit/:chain", governorCtrl.GetNotionalLimitByChainID) + governorNotional.Get("/available/", governorCtrl.GetAvailableNotional) + governorNotional.Get("/available/:chain", governorCtrl.GetAvailableNotionalByChainID) + governorNotional.Get("/max_available/:chain", governorCtrl.GetMaxNotionalAvailableByChainID) + + enqueueVaas := governor.Group("/enqueued_vaas") + enqueueVaas.Get("/", governorCtrl.GetEnqueueVaas) + enqueueVaas.Get("/:chain", governorCtrl.GetEnqueuedVaasByChainID) +} diff --git a/api/handlers/vaa/controller.go b/api/routes/wormscan/vaa/controller.go similarity index 69% rename from api/handlers/vaa/controller.go rename to api/routes/wormscan/vaa/controller.go index 5e0942b0..2e85ec78 100644 --- a/api/handlers/vaa/controller.go +++ b/api/routes/wormscan/vaa/controller.go @@ -5,19 +5,19 @@ import ( "strconv" "github.com/gofiber/fiber/v2" + "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" "go.uber.org/zap" ) // Controller definition. type Controller struct { - srv *Service + srv *vaa.Service logger *zap.Logger } // NewController create a new controler. -func NewController(serv *Service, logger *zap.Logger) *Controller { +func NewController(serv *vaa.Service, logger *zap.Logger) *Controller { return &Controller{srv: serv, logger: logger.With(zap.String("module", "VaaController"))} } @@ -123,56 +123,6 @@ func (c *Controller) FindById(ctx *fiber.Ctx) error { return ctx.JSON(vaa) } -// FindSignedVAAByID godoc -// @Description get a VAA []byte from a chainID, emitter address and sequence. -// @Tags Guardian -// @ID guardians-find-signed-vaa -// @Param chain_id path integer true "id of the blockchain" -// @Param emitter path string true "address of the emitter" -// @Param seq path integer true "sequence of the VAA" -// @Success 200 {object} object{vaaBytes=[]byte} -// @Failure 400 -// @Failure 500 -// @Router /v1/signed_vaa/{chain_id}/{emitter}/{seq} [get] -func (c *Controller) FindSignedVAAByID(ctx *fiber.Ctx) error { - chainID, emitter, seq, err := middleware.ExtractVAAParams(ctx, c.logger) - if err != nil { - return err - } - - // TODO - // check chainID is not Pyth. Pyth message are not stored with the other vaa. - //if ChainIDPythNet == chainID { - // return response.NewApiError(ctx, fiber.StatusBadRequest, response.InvalidParam, - // "not supported for PythNet", nil) - //} - vaa, err := c.srv.FindById(ctx.Context(), chainID, *emitter, strconv.FormatUint(seq, 10)) - if err != nil { - return err - } - response := struct { - VaaBytes []byte `json:"vaaBytes"` - }{ - VaaBytes: vaa.Data.Vaa, - } - return ctx.JSON(response) -} - -// FindSignedBatchVAAByID godoc -// @Description get a batch of VAA []byte from a chainID, emitter address and sequence. -// @Tags Guardian -// @ID guardians-find-signed-batch-vaa -// @Param chain_id path integer true "id of the blockchain" -// @Param emitter path string true "address of the emitter" -// @Param seq path integer true "sequence of the VAA" -// @Success 200 {object} object{vaaBytes=[]byte} -// @Failure 400 -// @Failure 500 -// @Router /v1/signed_batch_vaa/{chain_id}/{emitter}/sequence/{seq} [get] -func (c *Controller) FindSignedBatchVAAByID(ctx *fiber.Ctx) error { - return response.NewApiError(ctx, fiber.StatusNotImplemented, response.Unimplemented, "not yet implemented", nil) -} - // GetVaaCount godoc // @Description Returns the total number of VAAs emitted for each blockchain. // @Tags Wormscan