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  }