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 }