wormhole-explorer/api/handlers/vaa/service.go

274 lines
7.8 KiB
Go

package vaa
import (
"context"
"errors"
"fmt"
"strconv"
errs "github.com/wormhole-foundation/wormhole-explorer/api/internal/errors"
"github.com/wormhole-foundation/wormhole-explorer/api/internal/pagination"
"github.com/wormhole-foundation/wormhole-explorer/api/response"
"github.com/wormhole-foundation/wormhole-explorer/common/client/cache"
vaaPayloadParser "github.com/wormhole-foundation/wormhole-explorer/common/client/parser"
"github.com/wormhole-foundation/wormhole-explorer/common/types"
sdk "github.com/wormhole-foundation/wormhole/sdk/vaa"
"go.uber.org/zap"
)
// Service definition.
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, parseVaaFunc vaaPayloadParser.ParseVaaFunc, logger *zap.Logger) *Service {
s := Service{
repo: r,
getCacheFunc: getCacheFunc,
parseVaaFunc: parseVaaFunc,
logger: logger.With(zap.String("module", "VaaService")),
}
return &s
}
// FindAllParams passes input data to the function `FindAll`.
type FindAllParams struct {
Pagination *pagination.Pagination
TxHash *types.TxHash
IncludeParsedPayload bool
AppId string
}
// FindAll returns all VAAs.
func (s *Service) FindAll(
ctx context.Context,
params *FindAllParams,
) (*response.Response[[]*VaaDoc], error) {
// Populate query parameters
query := Query().
IncludeParsedPayload(params.IncludeParsedPayload)
if params.Pagination != nil {
query.SetPagination(params.Pagination)
}
if params.TxHash != nil {
query.SetTxHash(params.TxHash.String())
}
if params.AppId != "" {
query.SetAppId(params.AppId)
}
// Execute the database query
//
// Unfortunately, for Aptos and Solana, the real transaction hashes are stored
// in a different collection from other chains.
//
// This block of code has additional logic to handle that case.
var err error
var vaas []*VaaDoc
if query.txHash != "" {
vaas, err = s.repo.FindVaasByTxHashWorkaround(ctx, query)
} else {
vaas, err = s.repo.FindVaas(ctx, query)
}
if err != nil {
return nil, err
}
// Return the matching documents
res := response.Response[[]*VaaDoc]{Data: vaas}
return &res, nil
}
// FindByChain get all the vaa by chainID.
func (s *Service) FindByChain(
ctx context.Context,
chain sdk.ChainID,
p *pagination.Pagination,
) (*response.Response[[]*VaaDoc], error) {
query := Query().
SetChain(chain).
SetPagination(p).
IncludeParsedPayload(false)
vaas, err := s.repo.FindVaas(ctx, query)
res := response.Response[[]*VaaDoc]{Data: vaas}
return &res, err
}
// FindByEmitterParams contains the input parameters for the function `FindByEmitter`.
type FindByEmitterParams struct {
EmitterChain sdk.ChainID
EmitterAddress *types.Address
ToChain *sdk.ChainID
IncludeParsedPayload bool
Pagination *pagination.Pagination
}
// FindByEmitter get all the vaa by chainID and emitter address.
func (s *Service) FindByEmitter(
ctx context.Context,
params *FindByEmitterParams,
) (*response.Response[[]*VaaDoc], error) {
query := Query().
SetChain(params.EmitterChain).
SetEmitter(params.EmitterAddress.Hex()).
SetPagination(params.Pagination).
IncludeParsedPayload(params.IncludeParsedPayload)
// In most cases, the data is obtained from the VAA collection.
//
// The special case of filtering VAAs by `toChain` requires querying
// the data from a different collection.
var vaas []*VaaDoc
var err error
if params.ToChain != nil {
vaas, err = s.repo.FindVaasByEmitterAndToChain(ctx, query, *params.ToChain)
} else {
vaas, err = s.repo.FindVaas(ctx, query)
}
res := response.Response[[]*VaaDoc]{Data: vaas}
return &res, err
}
// If the parameter [payload] is true, the parse payload is added in the response.
func (s *Service) FindById(
ctx context.Context,
chain sdk.ChainID,
emitter *types.Address,
seq string,
includeParsedPayload bool,
) (*response.Response[*VaaDoc], error) {
// check vaa sequence indexed
isVaaNotIndexed := s.discardVaaNotIndexed(ctx, chain, emitter, seq)
if isVaaNotIndexed {
return nil, errs.ErrNotFound
}
// execute the database query
vaa, err := s.findById(ctx, chain, emitter, seq, includeParsedPayload)
if err != nil {
return &response.Response[*VaaDoc]{}, err
}
// return matching documents
resp := response.Response[*VaaDoc]{Data: vaa}
return &resp, err
}
// findById get a vaa by chainID, emitter address and sequence number.
func (s *Service) findById(
ctx context.Context,
chain sdk.ChainID,
emitter *types.Address,
seq string,
includeParsedPayload bool,
) (*VaaDoc, error) {
// query matching documents from the database
query := Query().
SetChain(chain).
SetEmitter(emitter.Hex()).
SetSequence(seq).
IncludeParsedPayload(includeParsedPayload)
docs, err := s.repo.FindVaas(ctx, query)
if err != nil {
return nil, err
}
// we're expecting exactly one document
if len(docs) == 0 {
return nil, errs.ErrNotFound
}
if len(docs) > 1 {
requestID := fmt.Sprintf("%v", ctx.Value("requestid"))
s.logger.Error("can not get more that one vaa by chainID/address/sequence",
zap.Any("q", query),
zap.String("requestID", requestID))
return nil, errs.ErrInternalError
}
return docs[0], nil
}
// GetVaaCount get a list a list of vaa count grouped by chainID.
func (s *Service) GetVaaCount(ctx context.Context) (*response.Response[[]*VaaStats], error) {
q := Query()
stats, err := s.repo.GetVaaCount(ctx, q)
res := response.Response[[]*VaaStats]{Data: stats}
return &res, err
}
// discardVaaNotIndexed discard a vaa request if the input sequence for a chainID, address is greatter than or equals
// the cached value of the sequence for this chainID, address.
// If the sequence does not exist we can not discard the request.
func (s *Service) discardVaaNotIndexed(ctx context.Context, chain sdk.ChainID, emitter *types.Address, seq string) bool {
key := fmt.Sprintf("%s:%d:%s", "wormscan:vaa-max-sequence", chain, emitter.Hex())
sequence, err := s.getCacheFunc(ctx, key)
if err != nil {
if errors.Is(err, cache.ErrInternal) {
requestID := fmt.Sprintf("%v", ctx.Value("requestid"))
s.logger.Error("encountered an internal error while getting value from cache",
zap.Error(err),
zap.String("requestID", requestID),
)
}
return false
}
inputSquence, err := strconv.ParseUint(seq, 10, 64)
if err != nil {
requestID := fmt.Sprintf("%v", ctx.Value("requestid"))
s.logger.Error("error invalid input sequence number",
zap.Error(err), zap.String("seq", seq), zap.String("requestID", requestID))
return false
}
cacheSequence, err := strconv.ParseUint(sequence, 10, 64)
if err != nil {
requestID := fmt.Sprintf("%v", ctx.Value("requestid"))
s.logger.Error("error invalid cached sequence number",
zap.Error(err), zap.String("sequence", sequence), zap.String("requestID", requestID))
return false
}
// Check that the input sequence is indexed.
if cacheSequence >= inputSquence {
return false
}
return true
}
// ParseVaa parse a vaa payload.
func (s *Service) ParseVaa(ctx context.Context, vaaByte []byte) (any, error) {
// unmarshal vaa
vaa, err := sdk.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 vaaPayloadParser.ParseVaaWithStandarizedPropertiesdResponse{}, 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
}