github.com/mavryk-network/mvgo@v1.19.9/rpc/client.go (about) 1 // Copyright (c) 2020-2022 Blockwatch Data Inc. 2 // Author: alex@blockwatch.cc 3 4 package rpc 5 6 import ( 7 "bytes" 8 "context" 9 "encoding/json" 10 "fmt" 11 "io" 12 "net/http" 13 "net/http/httputil" 14 "net/url" 15 "os" 16 "strings" 17 18 "github.com/mavryk-network/mvgo/mavryk" 19 "github.com/mavryk-network/mvgo/signer" 20 21 "github.com/echa/log" 22 ) 23 24 const ( 25 libraryVersion = "1.17.0" 26 userAgent = "mvgo/v" + libraryVersion 27 mediaType = "application/json" 28 ipfsUrl = "https://ipfs.io" 29 ) 30 31 // Client manages communication with a Tezos RPC server. 32 type Client struct { 33 // HTTP client used to communicate with the Tezos node API. 34 client *http.Client 35 // Base URL for API requests. 36 BaseURL *url.URL 37 // Base URL for IPFS requests. 38 IpfsURL *url.URL 39 // User agent name for client. 40 UserAgent string 41 // Optional API key for protected endpoints 42 ApiKey string 43 // The chain the client will query. 44 ChainId mavryk.ChainIdHash 45 // The current chain configuration. 46 Params *mavryk.Params 47 // An active event observer to watch for operation inclusion 48 BlockObserver *Observer 49 // An active event observer to watch for operation posting to the mempool 50 MempoolObserver *Observer 51 // A default signer used for transaction sending 52 Signer signer.Signer 53 // MetadataMode defines the metadata reconstruction mode used for fetching 54 // block and operation receipts. Set this mode to `always` if an RPC node prunes 55 // metadata (i.e. you see metadata too large in certain operations) 56 MetadataMode MetadataMode 57 // Close connections. This may help with EOF errors from unexpected 58 // connection close by Tezos RPC. 59 CloseConns bool 60 // Log is the logger implementation used by this client 61 Log log.Logger 62 } 63 64 // NewClient returns a new Tezos RPC client. 65 func NewClient(baseURL string, httpClient *http.Client) (*Client, error) { 66 if httpClient == nil { 67 httpClient = http.DefaultClient 68 } 69 if !strings.HasPrefix(baseURL, "http") { 70 baseURL = "http://" + baseURL 71 } 72 u, err := url.Parse(baseURL) 73 if err != nil { 74 return nil, err 75 } 76 q := u.Query() 77 key := q.Get("api_key") 78 if key != "" { 79 q.Del("api_key") 80 u.RawQuery = q.Encode() 81 } else { 82 key = os.Getenv("MVGO_API_KEY") 83 } 84 ipfs, _ := url.Parse(ipfsUrl) 85 c := &Client{ 86 client: httpClient, 87 BaseURL: u, 88 IpfsURL: ipfs, 89 UserAgent: userAgent, 90 ApiKey: key, 91 BlockObserver: NewObserver(), 92 MempoolObserver: NewObserver(), 93 MetadataMode: MetadataModeAlways, 94 Log: logger, 95 } 96 return c, nil 97 } 98 99 func (c *Client) Init(ctx context.Context) error { 100 return c.ResolveChainConfig(ctx) 101 } 102 103 func (c *Client) UseIpfsUrl(uri string) error { 104 u, err := url.Parse(uri) 105 if err != nil { 106 return err 107 } 108 c.IpfsURL = u 109 return nil 110 } 111 112 func (c *Client) Client() *http.Client { 113 return c.client 114 } 115 116 func (c *Client) Listen() { 117 // start observers 118 c.BlockObserver.Listen(c) 119 c.MempoolObserver.ListenMempool(c) 120 } 121 122 func (c *Client) Close() { 123 c.BlockObserver.Close() 124 c.MempoolObserver.Close() 125 } 126 127 func (c *Client) ResolveChainConfig(ctx context.Context) error { 128 id, err := c.GetChainId(ctx) 129 if err != nil { 130 return err 131 } 132 c.ChainId = id 133 p, err := c.GetParams(ctx, Head) 134 if err != nil { 135 return err 136 } 137 c.Params = p 138 return nil 139 } 140 141 func (c *Client) Get(ctx context.Context, urlpath string, result interface{}) error { 142 req, err := c.NewRequest(ctx, http.MethodGet, urlpath, nil) 143 if err != nil { 144 return err 145 } 146 return c.Do(req, result) 147 } 148 149 func (c *Client) GetAsync(ctx context.Context, urlpath string, mon Monitor) error { 150 req, err := c.NewRequest(ctx, http.MethodGet, urlpath, nil) 151 if err != nil { 152 return err 153 } 154 return c.DoAsync(req, mon) 155 } 156 157 func (c *Client) Put(ctx context.Context, urlpath string, body, result interface{}) error { 158 req, err := c.NewRequest(ctx, http.MethodPut, urlpath, body) 159 if err != nil { 160 return err 161 } 162 return c.Do(req, result) 163 } 164 165 func (c *Client) Post(ctx context.Context, urlpath string, body, result interface{}) error { 166 req, err := c.NewRequest(ctx, http.MethodPost, urlpath, body) 167 if err != nil { 168 return err 169 } 170 return c.Do(req, result) 171 } 172 173 // NewRequest creates a Tezos RPC request. 174 func (c *Client) NewRequest(ctx context.Context, method, urlStr string, body interface{}) (*http.Request, error) { 175 rel, err := url.Parse(urlStr) 176 if err != nil { 177 return nil, err 178 } 179 180 u := c.BaseURL.ResolveReference(rel) 181 182 buf := new(bytes.Buffer) 183 if body != nil { 184 err = json.NewEncoder(buf).Encode(body) 185 if err != nil { 186 return nil, err 187 } 188 } 189 190 req, err := http.NewRequest(method, u.String(), buf) 191 if err != nil { 192 return nil, err 193 } 194 req = req.WithContext(ctx) 195 req.Close = c.CloseConns 196 197 req.Header.Add("Content-Type", mediaType) 198 req.Header.Add("Accept", mediaType) 199 req.Header.Add("User-Agent", c.UserAgent) 200 if c.ApiKey != "" { 201 req.Header.Add("X-Api-Key", c.ApiKey) 202 } 203 204 c.logDebugOnly(func() { 205 c.Log.Debugf("%s %s %s", req.Method, req.URL, req.Proto) 206 }) 207 c.logTraceOnly(func() { 208 d, _ := httputil.DumpRequest(req, true) 209 c.Log.Trace(string(d)) 210 }) 211 212 return req, nil 213 } 214 215 func (c *Client) handleResponse(resp *http.Response, v interface{}) error { 216 return json.NewDecoder(resp.Body).Decode(v) 217 } 218 219 func (c *Client) handleResponseMonitor(ctx context.Context, resp *http.Response, mon Monitor) { 220 // decode stream 221 dec := json.NewDecoder(resp.Body) 222 223 // close body when stream stopped 224 defer func() { 225 io.Copy(io.Discard, resp.Body) 226 resp.Body.Close() 227 }() 228 229 for { 230 chunkVal := mon.New() 231 if err := dec.Decode(chunkVal); err != nil { 232 select { 233 case <-mon.Closed(): 234 return 235 case <-ctx.Done(): 236 return 237 default: 238 } 239 if err == io.EOF || err == io.ErrUnexpectedEOF { 240 mon.Err(io.EOF) 241 return 242 } 243 mon.Err(fmt.Errorf("rpc: %v", err)) 244 return 245 } 246 select { 247 case <-mon.Closed(): 248 return 249 case <-ctx.Done(): 250 return 251 default: 252 mon.Send(ctx, chunkVal) 253 } 254 } 255 } 256 257 // Do retrieves values from the API and marshals them into the provided interface. 258 func (c *Client) Do(req *http.Request, v interface{}) error { 259 resp, err := c.client.Do(req) 260 if err != nil { 261 if e, ok := err.(*url.Error); ok { 262 return e.Err 263 } 264 return err 265 } 266 267 defer func() { 268 io.Copy(io.Discard, resp.Body) 269 resp.Body.Close() 270 }() 271 272 if resp.StatusCode == http.StatusNoContent { 273 return nil 274 } 275 276 c.logTraceOnly((func() { 277 d, _ := httputil.DumpResponse(resp, true) 278 c.Log.Trace(string(d)) 279 })) 280 281 statusClass := resp.StatusCode / 100 282 if statusClass == 2 { 283 if v == nil { 284 return nil 285 } 286 return c.handleResponse(resp, v) 287 } 288 289 return c.handleError(resp) 290 } 291 292 // DoAsync retrieves values from the API and sends responses using the provided monitor. 293 func (c *Client) DoAsync(req *http.Request, mon Monitor) error { 294 //nolint:bodyclose 295 resp, err := c.client.Do(req) 296 if err != nil { 297 if e, ok := err.(*url.Error); ok { 298 return e.Err 299 } 300 return err 301 } 302 303 if resp.StatusCode == http.StatusNoContent { 304 io.Copy(io.Discard, resp.Body) 305 resp.Body.Close() 306 return nil 307 } 308 309 statusClass := resp.StatusCode / 100 310 if statusClass == 2 { 311 if mon != nil { 312 go func() { 313 c.handleResponseMonitor(req.Context(), resp, mon) 314 }() 315 return nil 316 } 317 } else { 318 return c.handleError(resp) 319 } 320 io.Copy(io.Discard, resp.Body) 321 resp.Body.Close() 322 return nil 323 } 324 325 func (c *Client) handleError(resp *http.Response) error { 326 body, err := io.ReadAll(resp.Body) 327 if err != nil { 328 return err 329 } 330 331 httpErr := httpError{ 332 request: resp.Request.Method + " " + resp.Request.URL.RequestURI(), 333 status: resp.Status, 334 statusCode: resp.StatusCode, 335 body: bytes.ReplaceAll(body, []byte("\n"), []byte{}), 336 } 337 338 if resp.StatusCode < 500 || !strings.Contains(resp.Header.Get("Content-Type"), "application/json") { 339 // Other errors with unknown body format (usually human readable string) 340 return &httpErr 341 } 342 343 var errs Errors 344 if err := json.Unmarshal(body, &errs); err != nil { 345 return &plainError{&httpErr, fmt.Sprintf("rpc: error decoding RPC error: %v", err)} 346 } 347 348 if len(errs) == 0 { 349 c.Log.Errorf("rpc: error decoding RPC error response: %v", err) 350 return &httpErr 351 } 352 353 return &rpcError{ 354 httpError: &httpErr, 355 errors: errs, 356 } 357 }