wormhole-explorer/api/main.go

215 lines
7.7 KiB
Go

package main
import (
"context"
_ "embed"
"fmt"
"net/http"
"os"
"os/signal"
"strconv"
"syscall"
"github.com/ansrivas/fiberprometheus/v2"
"github.com/go-redis/redis/v8"
"github.com/gofiber/adaptor/v2"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/cors"
"github.com/gofiber/fiber/v2/middleware/logger"
"github.com/gofiber/fiber/v2/middleware/pprof"
"github.com/gofiber/fiber/v2/middleware/requestid"
"github.com/improbable-eng/grpc-web/go/grpcweb"
influxdb2 "github.com/influxdata/influxdb-client-go/v2"
"github.com/wormhole-foundation/wormhole-explorer/api/handlers/address"
"github.com/wormhole-foundation/wormhole-explorer/api/handlers/governor"
"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"
"github.com/wormhole-foundation/wormhole-explorer/api/handlers/transactions"
"github.com/wormhole-foundation/wormhole-explorer/api/handlers/vaa"
"github.com/wormhole-foundation/wormhole-explorer/api/internal/config"
"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"
wormscanCache "github.com/wormhole-foundation/wormhole-explorer/common/client/cache"
wormscanNotionalCache "github.com/wormhole-foundation/wormhole-explorer/common/client/cache/notional"
xlogger "github.com/wormhole-foundation/wormhole-explorer/common/logger"
"go.uber.org/zap"
)
//go:embed docs/swagger.json
var swagger []byte
// GetSwagger godoc
// @Description Returns the swagger specification for this API.
// @Tags Wormscan
// @ID swagger
// @Success 200 {object} object
// @Failure 400
// @Failure 500
// @Router /swagger.json [get]
func GetSwagger(ctx *fiber.Ctx) error {
written, err := ctx.
Response().
BodyWriter().
Write(swagger)
if written != len(swagger) {
return fmt.Errorf("partial write to response body: wrote %d bytes, expected %d", written, len(swagger))
}
return err
}
// @title Wormhole Guardian API
// @version 1.0
// @description Wormhole Guardian API
// @description To get information from the Wormhole Network.
// @description Check each endpoint documentation for more information.
// @termsOfService https://wormhole.com/
// @contact.name API Support
// @contact.url http://wormhole.com/support
// @contact.email info@wormhole.com
// @license.name Apache 2.0
// @license.url http://www.apache.org/licenses/LICENSE-2.0.html
// @BasePath /v1
func main() {
appCtx, cancel := context.WithCancel(context.Background())
defer cancel()
// Grab config
cfg, err := config.Get()
if err != nil {
fmt.Fprint(os.Stderr, "Error parsing configuration")
panic(err)
}
// Logging
rootLogger := xlogger.New("wormhole-api", xlogger.WithLevel(cfg.LogLevel))
// Setup DB
cli, err := db.Connect(appCtx, cfg.DB.URL)
if err != nil {
panic(err)
}
db := cli.Database(cfg.DB.Name)
// Get cache get function
cache, notionalCache := NewCache(appCtx, cfg, rootLogger)
//InfluxDB client
influxCli := newInfluxClient(cfg.Influx.URL, cfg.Influx.Token)
// Set up repositories
addressRepo := address.NewRepository(db, rootLogger)
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)
transactionsRepo := transactions.NewRepository(influxCli, cfg.Influx.Organization, cfg.Influx.Bucket, db, rootLogger)
// Set up services
addressService := address.NewService(addressRepo, rootLogger)
vaaService := vaa.NewService(vaaRepo, cache.Get, rootLogger)
obsService := observations.NewService(obsRepo, rootLogger)
governorService := governor.NewService(governorRepo, rootLogger)
infrastructureService := infrastructure.NewService(infrastructureRepo, rootLogger)
heartbeatsService := heartbeats.NewService(heartbeatsRepo, rootLogger)
transactionsService := transactions.NewService(transactionsRepo, rootLogger)
// Set up a custom error handler
response.SetEnableStackTrace(*cfg)
app := fiber.New(fiber.Config{ErrorHandler: middleware.ErrorHandler})
// 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",
}))
if cfg.PprofEnabled {
app.Use(pprof.New())
}
// Set up route handlers
app.Get("/swagger.json", GetSwagger)
wormscan.RegisterRoutes(app, rootLogger, addressService, vaaService, obsService, governorService, infrastructureService, transactionsService)
guardian.RegisterRoutes(cfg, app, rootLogger, vaaService, governorService, heartbeatsService)
// Set up gRPC handlers
handler := rpcApi.NewHandler(vaaService, heartbeatsService, governorService, rootLogger, cfg.P2pNetwork)
grpcServer := rpcApi.NewServer(handler, rootLogger)
grpcWebServer := grpcweb.WrapServer(grpcServer)
app.Use(
adaptor.HTTPMiddleware(func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if grpcWebServer.IsGrpcWebRequest(r) {
grpcWebServer.ServeHTTP(w, r)
} else {
next.ServeHTTP(w, r)
}
})
}))
go func() {
if err := app.Listen(":" + strconv.Itoa(cfg.PORT)); err != nil {
rootLogger.Error("http listen", zap.Error(err))
panic(err)
}
}()
// Waiting for signal
sigterm := make(chan os.Signal, 1)
signal.Notify(sigterm, syscall.SIGINT, syscall.SIGTERM)
select {
case <-appCtx.Done():
rootLogger.Warn("terminating with root context cancelled.")
case signal := <-sigterm:
rootLogger.Info("terminating with signal.", zap.String("signal", signal.String()))
}
rootLogger.Info("cleanup tasks...")
rootLogger.Info("shutdown server...")
app.Shutdown()
rootLogger.Info("close pubsub notional...")
notionalCache.Close()
rootLogger.Info("close cache...")
cache.Close()
rootLogger.Info("finished successfully wormhole api")
}
// NewCache get a CacheGetFunc to get a value by a Key from cache and a CacheReadable to get a value by a Key from notional local cache.
func NewCache(ctx context.Context, cfg *config.AppConfig, logger *zap.Logger) (wormscanCache.CacheReadable, wormscanNotionalCache.NotionalLocalCacheReadable) {
// if run mode is development with cache is disabled, return a dummy cache client and a dummy notional cache client.
if cfg.RunMode == config.RunModeDevelopmernt && !cfg.Cache.Enabled {
dummyCacheClient := wormscanCache.NewDummyCacheClient()
dummyNotionalCache := wormscanNotionalCache.NewDummyNotionalCache()
return dummyCacheClient, dummyNotionalCache
}
// if we are not in development mode, use a distributed cache and for notional a pubsub to sync local cache.
redisClient := redis.NewClient(&redis.Options{Addr: cfg.Cache.URL})
// get cache client
cacheClient, _ := wormscanCache.NewCacheClient(redisClient, cfg.Cache.Enabled, logger)
// get notional cache client and init load to local cache
notionalCache, _ := wormscanNotionalCache.NewNotionalCache(ctx, redisClient, cfg.Cache.Channel, logger)
notionalCache.Init(ctx)
return cacheClient, notionalCache
}
func newInfluxClient(url, token string) influxdb2.Client {
return influxdb2.NewClient(url, token)
}