code.vegaprotocol.io/vega@v0.79.0/blockexplorer/api/grpc/implementation.go (about)

     1  // Copyright (C) 2023 Gobalsky Labs Limited
     2  //
     3  // This program is free software: you can redistribute it and/or modify
     4  // it under the terms of the GNU Affero General Public License as
     5  // published by the Free Software Foundation, either version 3 of the
     6  // License, or (at your option) any later version.
     7  //
     8  // This program is distributed in the hope that it will be useful,
     9  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    10  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    11  // GNU Affero General Public License for more details.
    12  //
    13  // You should have received a copy of the GNU Affero General Public License
    14  // along with this program.  If not, see <http://www.gnu.org/licenses/>.
    15  
    16  package grpc
    17  
    18  import (
    19  	"context"
    20  	"errors"
    21  
    22  	"code.vegaprotocol.io/vega/blockexplorer/entities"
    23  	"code.vegaprotocol.io/vega/blockexplorer/store"
    24  	"code.vegaprotocol.io/vega/logging"
    25  	pb "code.vegaprotocol.io/vega/protos/blockexplorer/api/v1"
    26  	types "code.vegaprotocol.io/vega/protos/vega"
    27  	"code.vegaprotocol.io/vega/version"
    28  
    29  	"google.golang.org/grpc/codes"
    30  	"google.golang.org/grpc/status"
    31  )
    32  
    33  var ErrNotMapped = errors.New("error not mapped")
    34  
    35  type blockExplorerAPI struct {
    36  	Config
    37  	pb.UnimplementedBlockExplorerServiceServer
    38  	store *store.Store
    39  	log   *logging.Logger
    40  }
    41  
    42  func NewBlockExplorerAPI(store *store.Store, config Config, log *logging.Logger) pb.BlockExplorerServiceServer {
    43  	log = log.Named(namedLogger)
    44  	log.SetLevel(config.Level.Get())
    45  
    46  	be := blockExplorerAPI{
    47  		Config: config,
    48  		store:  store,
    49  		log:    log.Named(namedLogger),
    50  	}
    51  	return &be
    52  }
    53  
    54  func (b *blockExplorerAPI) Info(ctx context.Context, _ *pb.InfoRequest) (*pb.InfoResponse, error) {
    55  	return &pb.InfoResponse{
    56  		Version:    version.Get(),
    57  		CommitHash: version.GetCommitHash(),
    58  	}, nil
    59  }
    60  
    61  func (b *blockExplorerAPI) GetTransaction(ctx context.Context, req *pb.GetTransactionRequest) (*pb.GetTransactionResponse, error) {
    62  	transaction, err := b.store.GetTransaction(ctx, req.Hash)
    63  	if err != nil {
    64  		c := codes.Internal
    65  		if errors.Is(err, store.ErrTxNotFound) {
    66  			c = codes.NotFound
    67  		} else if errors.Is(err, store.ErrMultipleTxFound) {
    68  			c = codes.FailedPrecondition
    69  		}
    70  		return nil, apiError(c, err)
    71  	}
    72  
    73  	resp := pb.GetTransactionResponse{
    74  		Transaction: transaction,
    75  	}
    76  
    77  	return &resp, nil
    78  }
    79  
    80  func (b *blockExplorerAPI) ListTransactions(ctx context.Context, req *pb.ListTransactionsRequest) (*pb.ListTransactionsResponse, error) {
    81  	var before, after *entities.TxCursor
    82  	var first, last uint32
    83  
    84  	if req.Before != nil && req.After != nil && (req.First > 0 || req.Last > 0) {
    85  		return nil, apiError(codes.InvalidArgument, errors.New("cannot use neither limits `first`, nor `last` when both cursors `before` and `after` are set"))
    86  	}
    87  
    88  	if req.First > 0 && req.Last > 0 {
    89  		return nil, apiError(codes.InvalidArgument, errors.New("cannot use both limits `first` and `last` within the same query"))
    90  	}
    91  
    92  	if req.Before != nil {
    93  		cursor, err := entities.TxCursorFromString(*req.Before)
    94  		if err != nil {
    95  			return nil, apiError(codes.InvalidArgument, err)
    96  		}
    97  		before = &cursor
    98  		last = b.MaxPageSizeDefault
    99  	}
   100  
   101  	if req.After != nil {
   102  		cursor, err := entities.TxCursorFromString(*req.After)
   103  		if err != nil {
   104  			return nil, apiError(codes.InvalidArgument, err)
   105  		}
   106  		after = &cursor
   107  		first = b.MaxPageSizeDefault
   108  	}
   109  
   110  	if before != nil && after != nil {
   111  		// The order of the parameters may seem odd, but this is expected as we have
   112  		// to keep in mind the natural order of the block-explorer is reverse-chronological.
   113  		// so, given transactions 4.2, 4.1, 3.2, 3.1, 2.2, when applying the window between
   114  		// 3.1 and 4.2, then we have to set after to 3.1 and before to 4.2.
   115  		// So effectively, after is the start and before is the end of the set.
   116  		if entities.AreValidCursorBoundaries(after, before) {
   117  			return nil, apiError(codes.InvalidArgument, errors.New("cursors `before` and `after` do not create a valid window"))
   118  		}
   119  	}
   120  
   121  	if req.First > 0 {
   122  		if req.Before != nil {
   123  			return nil, apiError(codes.InvalidArgument, errors.New("cannot use cursor `before` when using limit `first`"))
   124  		}
   125  		first = req.First
   126  	} else if req.Last > 0 {
   127  		if req.After != nil {
   128  			return nil, apiError(codes.InvalidArgument, errors.New("cannot use cursor `after` when using limit `last`"))
   129  		}
   130  		last = req.Last
   131  	}
   132  
   133  	// Entering this condition means there is no pagination set, so it defaults
   134  	// to listing the MaxPageSizeDefault newest transactions.
   135  	// Note, setting limits on a cursor window is not supported.
   136  	if !(before != nil && after != nil) && first == 0 && last == 0 {
   137  		first = b.MaxPageSizeDefault
   138  	}
   139  
   140  	transactions, err := b.store.ListTransactions(ctx,
   141  		req.Filters,
   142  		req.CmdTypes,
   143  		req.ExcludeCmdTypes,
   144  		req.Parties,
   145  		first,
   146  		after,
   147  		last,
   148  		before,
   149  	)
   150  	if err != nil {
   151  		return nil, apiError(codes.Internal, err)
   152  	}
   153  
   154  	return &pb.ListTransactionsResponse{
   155  		Transactions: transactions,
   156  	}, nil
   157  }
   158  
   159  // errorMap contains a mapping between errors and Vega numeric error codes.
   160  var errorMap = map[string]int32{
   161  	// General
   162  	ErrNotMapped.Error():             10000,
   163  	store.ErrTxNotFound.Error():      10001,
   164  	store.ErrMultipleTxFound.Error(): 10002,
   165  }
   166  
   167  // apiError is a helper function to build the Vega specific Error Details that
   168  // can be returned by gRPC API and therefore also REST, GraphQL will be mapped too.
   169  // It takes a standardised grpcCode, a Vega specific apiError, and optionally one
   170  // or more internal errors (error from the core, rather than API).
   171  func apiError(grpcCode codes.Code, apiError error) error {
   172  	s := status.Newf(grpcCode, "%v error", grpcCode)
   173  	// Create the API specific error detail for error e.g. missing party ID
   174  	detail := types.ErrorDetail{
   175  		Message: apiError.Error(),
   176  	}
   177  	// Lookup the API specific error in the table, return not found/not mapped
   178  	// if a code has not yet been added to the map, can happen if developer misses
   179  	// a step, periodic checking/ownership of API package can keep this up to date.
   180  	vegaCode, found := errorMap[apiError.Error()]
   181  	if found {
   182  		detail.Code = vegaCode
   183  	} else {
   184  		detail.Code = errorMap[ErrNotMapped.Error()]
   185  	}
   186  	// Pack the Vega domain specific errorDetails into the status returned by gRPC domain.
   187  	s, _ = s.WithDetails(&detail)
   188  	return s.Err()
   189  }