code.vegaprotocol.io/vega@v0.79.0/core/faucet/service.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 faucet 17 18 import ( 19 "context" 20 "encoding/json" 21 "errors" 22 "fmt" 23 "io/ioutil" 24 "net/http" 25 26 vfmt "code.vegaprotocol.io/vega/libs/fmt" 27 vghttp "code.vegaprotocol.io/vega/libs/http" 28 "code.vegaprotocol.io/vega/libs/num" 29 "code.vegaprotocol.io/vega/libs/proto" 30 vgrand "code.vegaprotocol.io/vega/libs/rand" 31 "code.vegaprotocol.io/vega/logging" 32 "code.vegaprotocol.io/vega/paths" 33 types "code.vegaprotocol.io/vega/protos/vega" 34 api "code.vegaprotocol.io/vega/protos/vega/api/v1" 35 commandspb "code.vegaprotocol.io/vega/protos/vega/commands/v1" 36 37 "github.com/cenkalti/backoff" 38 "github.com/julienschmidt/httprouter" 39 "github.com/rs/cors" 40 "google.golang.org/grpc" 41 "google.golang.org/grpc/credentials/insecure" 42 ) 43 44 var ( 45 // ErrNotABuiltinAsset is raised when a party try to top up for a non builtin asset. 46 ErrNotABuiltinAsset = errors.New("asset is not a builtin asset") 47 48 // ErrAssetNotFound is raised when an asset id is not found. 49 ErrAssetNotFound = errors.New("asset was not found") 50 51 // ErrInvalidMintAmount is raised when the mint amount is too high. 52 ErrInvalidMintAmount = errors.New("mint amount is invalid") 53 54 HealthCheckResponse = struct { 55 Success bool `json:"success"` 56 }{true} 57 ) 58 59 type Faucet struct { 60 *httprouter.Router 61 62 log *logging.Logger 63 cfg Config 64 wallet *faucetWallet 65 s *http.Server 66 rl *vghttp.RateLimit 67 cfunc context.CancelFunc 68 stopCh chan struct{} 69 70 // node connections stuff 71 clt api.CoreServiceClient 72 coreclt api.CoreStateServiceClient 73 conn *grpc.ClientConn 74 } 75 76 type MintRequest struct { 77 Party string `json:"party"` 78 Amount string `json:"amount"` 79 Asset string `json:"asset"` 80 } 81 82 type MintResponse struct { 83 Success bool `json:"success"` 84 } 85 86 func NewService(log *logging.Logger, vegaPaths paths.Paths, cfg Config, passphrase string) (*Faucet, error) { 87 log = log.Named(namedLogger) 88 log.SetLevel(cfg.Level.Level) 89 90 wallet, err := loadWallet(vegaPaths, cfg.WalletName, passphrase) 91 if err != nil { 92 return nil, fmt.Errorf("could not load the faucet wallet %s: %w", cfg.WalletName, err) 93 } 94 95 nodeAddr := fmt.Sprintf("%v:%v", cfg.Node.IP, cfg.Node.Port) 96 conn, err := grpc.Dial(nodeAddr, grpc.WithTransportCredentials(insecure.NewCredentials())) 97 if err != nil { 98 return nil, fmt.Errorf("could not initialise the gPRC client: %w", err) 99 } 100 101 client := api.NewCoreServiceClient(conn) 102 coreClient := api.NewCoreStateServiceClient(conn) 103 ctx, cfunc := context.WithCancel(context.Background()) 104 105 rl, err := vghttp.NewRateLimit(ctx, cfg.RateLimit) 106 if err != nil { 107 cfunc() 108 return nil, fmt.Errorf("could not initialise the rate limiter: %v", err) 109 } 110 111 f := &Faucet{ 112 Router: httprouter.New(), 113 log: log, 114 cfg: cfg, 115 wallet: wallet, 116 clt: client, 117 coreclt: coreClient, 118 conn: conn, 119 cfunc: cfunc, 120 rl: rl, 121 stopCh: make(chan struct{}), 122 } 123 124 f.POST("/api/v1/mint", f.Mint) 125 f.GET("/api/v1/health", f.Health) 126 127 return f, nil 128 } 129 130 func (f *Faucet) Health(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { 131 writeSuccess(w, HealthCheckResponse, http.StatusOK) 132 } 133 134 func (f *Faucet) Mint(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { 135 req := MintRequest{} 136 if err := unmarshalBody(r, &req); err != nil { 137 writeError(w, newError(err.Error()), http.StatusBadRequest) 138 return 139 } 140 141 if len(req.Party) <= 0 { 142 writeError(w, newError("missing party field"), http.StatusBadRequest) 143 return 144 } 145 if len(req.Amount) <= 0 { 146 writeError(w, newError("amount need to be a > 0 unsigned integer"), http.StatusBadRequest) 147 return 148 } 149 amount, overflowed := num.UintFromString(req.Amount, 10) 150 if overflowed { 151 writeError(w, newError("amount overflowed or was not base 10"), http.StatusBadRequest) 152 } 153 154 if len(req.Asset) <= 0 { 155 writeError(w, newError("missing asset field"), http.StatusBadRequest) 156 return 157 } 158 159 if err := f.getAllowedAmount(r.Context(), amount, req.Asset); err != nil { 160 if errors.Is(err, ErrAssetNotFound) || errors.Is(err, ErrInvalidMintAmount) { 161 writeError(w, newError(err.Error()), http.StatusBadRequest) 162 return 163 } 164 writeError(w, newError(err.Error()), http.StatusInternalServerError) 165 return 166 } 167 168 // rate limit minting by source IP address, party, asset 169 ip, err := vghttp.RemoteAddr(r) 170 if err != nil { 171 writeError(w, newError(fmt.Sprintf("failed to get request remote address: %v", err)), http.StatusBadRequest) 172 return 173 } 174 rlkey := fmt.Sprintf("minting for party %s and asset %s", req.Party, req.Asset) 175 if err := f.rl.NewRequest(rlkey, ip); err != nil { 176 f.log.Debug("Mint denied - rate limit", 177 logging.String("ip", vfmt.Escape(ip)), 178 logging.String("rlkey", vfmt.Escape(rlkey)), 179 ) 180 writeError(w, newError(err.Error()), http.StatusForbidden) 181 return 182 } 183 184 ce := &commandspb.ChainEvent{ 185 Nonce: vgrand.NewNonce(), 186 Event: &commandspb.ChainEvent_Builtin{ 187 Builtin: &types.BuiltinAssetEvent{ 188 Action: &types.BuiltinAssetEvent_Deposit{ 189 Deposit: &types.BuiltinAssetDeposit{ 190 VegaAssetId: req.Asset, 191 PartyId: req.Party, 192 Amount: req.Amount, 193 }, 194 }, 195 }, 196 }, 197 } 198 199 msg, err := proto.Marshal(ce) 200 if err != nil { 201 writeError(w, newError("unable to marshal"), http.StatusInternalServerError) 202 return 203 } 204 205 sig, pubKey, err := f.wallet.Sign(msg) 206 if err != nil { 207 f.log.Error("unable to sign", logging.Error(err)) 208 writeError(w, newError("unable to sign crypto"), http.StatusInternalServerError) 209 return 210 } 211 212 preq := &api.PropagateChainEventRequest{ 213 Event: msg, 214 PubKey: pubKey, 215 Signature: sig, 216 } 217 218 var ok bool 219 err = backoff.Retry( 220 func() error { 221 resp, err := f.clt.PropagateChainEvent(context.Background(), preq) 222 if err != nil { 223 return err 224 } 225 ok = resp.Success 226 return nil 227 }, 228 backoff.WithMaxRetries(backoff.NewExponentialBackOff(), f.cfg.Node.Retries), 229 ) 230 if err != nil { 231 writeError(w, newError(err.Error()), http.StatusInternalServerError) 232 return 233 } 234 235 resp := MintResponse{ok} 236 writeSuccess(w, resp, http.StatusOK) 237 } 238 239 func (f *Faucet) getAllowedAmount(ctx context.Context, amount *num.Uint, asset string) error { 240 req := &api.ListAssetsRequest{ 241 Asset: asset, 242 } 243 resp, err := f.coreclt.ListAssets(ctx, req) 244 if err != nil { 245 return err 246 } 247 if len(resp.Assets) <= 0 { 248 return ErrAssetNotFound 249 } 250 source := resp.Assets[0].Details.GetBuiltinAsset() 251 if source == nil { 252 return ErrNotABuiltinAsset 253 } 254 maxAmount, overflowed := num.UintFromString(source.MaxFaucetAmountMint, 10) 255 if overflowed { 256 return ErrInvalidMintAmount 257 } 258 if maxAmount.LT(amount) { 259 return fmt.Errorf("amount request exceed maximal amount of %v: %w", maxAmount, ErrInvalidMintAmount) 260 } 261 262 return nil 263 } 264 265 func (f *Faucet) Start() error { 266 f.s = &http.Server{ 267 Addr: fmt.Sprintf("%s:%v", f.cfg.IP, f.cfg.Port), 268 Handler: cors.AllowAll().Handler(f), // middleware with cors 269 } 270 271 f.log.Info("starting faucet server", logging.String("address", f.s.Addr)) 272 273 errCh := make(chan error) 274 go func() { 275 errCh <- f.s.ListenAndServe() 276 }() 277 278 defer func() { 279 f.cfunc() 280 f.conn.Close() 281 }() 282 283 // close the rate limit 284 select { 285 case err := <-errCh: 286 return err 287 case <-f.stopCh: 288 f.s.Shutdown(context.Background()) 289 return nil 290 } 291 } 292 293 func (f *Faucet) Stop() error { 294 f.stopCh <- struct{}{} 295 return nil 296 } 297 298 func unmarshalBody(r *http.Request, into interface{}) error { 299 defer r.Body.Close() 300 body, err := ioutil.ReadAll(r.Body) 301 if err != nil { 302 return ErrInvalidRequest 303 } 304 return json.Unmarshal(body, into) 305 } 306 307 func writeError(w http.ResponseWriter, e error, status int) { 308 w.Header().Set("Content-Type", "application/json") 309 w.WriteHeader(status) 310 buf, _ := json.Marshal(e) 311 w.Write(buf) 312 } 313 314 func writeSuccess(w http.ResponseWriter, data interface{}, status int) { 315 w.Header().Set("Content-Type", "application/json") 316 w.WriteHeader(status) 317 buf, _ := json.Marshal(data) 318 w.Write(buf) 319 } 320 321 var ErrInvalidRequest = newError("invalid request") 322 323 type HTTPError struct { 324 ErrorStr string `json:"error"` 325 } 326 327 func (e HTTPError) Error() string { 328 return e.ErrorStr 329 } 330 331 func newError(e string) HTTPError { 332 return HTTPError{ 333 ErrorStr: e, 334 } 335 }