github.com/nspcc-dev/neo-go@v0.105.2-0.20240517133400-6be757af3eba/pkg/rpcclient/client.go (about)

     1  package rpcclient
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"encoding/json"
     7  	"errors"
     8  	"fmt"
     9  	"net"
    10  	"net/http"
    11  	"net/url"
    12  	"sync"
    13  	"sync/atomic"
    14  	"time"
    15  
    16  	"github.com/nspcc-dev/neo-go/pkg/config/netmode"
    17  	"github.com/nspcc-dev/neo-go/pkg/neorpc"
    18  	"github.com/nspcc-dev/neo-go/pkg/rpcclient/invoker"
    19  	"github.com/nspcc-dev/neo-go/pkg/util"
    20  )
    21  
    22  const (
    23  	defaultDialTimeout    = 4 * time.Second
    24  	defaultRequestTimeout = 4 * time.Second
    25  )
    26  
    27  // Client represents the middleman for executing JSON RPC calls
    28  // to remote NEO RPC nodes. Client is thread-safe and can be used from
    29  // multiple goroutines.
    30  type Client struct {
    31  	cli      *http.Client
    32  	endpoint *url.URL
    33  	ctx      context.Context
    34  	// ctxCancel is a cancel function aimed to send closing signal to the users of
    35  	// ctx.
    36  	ctxCancel func()
    37  	opts      Options
    38  	requestF  func(*neorpc.Request) (*neorpc.Response, error)
    39  
    40  	// reader is an Invoker that has no signers and uses current state,
    41  	// it's used to implement various getters. It'll be removed eventually,
    42  	// but for now it keeps Client's API compatibility.
    43  	reader *invoker.Invoker
    44  
    45  	cacheLock sync.RWMutex
    46  	// cache stores RPC node related information the client is bound to.
    47  	// cache is mostly filled in during Init(), but can also be updated
    48  	// during regular Client lifecycle.
    49  	cache cache
    50  
    51  	latestReqID atomic.Uint64
    52  	// getNextRequestID returns an ID to be used for the subsequent request creation.
    53  	// It is defined on Client, so that our testing code can override this method
    54  	// for the sake of more predictable request IDs generation behavior.
    55  	getNextRequestID func() uint64
    56  }
    57  
    58  // Options defines options for the RPC client.
    59  // All values are optional. If any duration is not specified,
    60  // a default of 4 seconds will be used.
    61  type Options struct {
    62  	// Cert is a client-side certificate, it doesn't work at the moment along
    63  	// with the other two options below.
    64  	Cert           string
    65  	Key            string
    66  	CACert         string
    67  	DialTimeout    time.Duration
    68  	RequestTimeout time.Duration
    69  	// Limit total number of connections per host. No limit by default.
    70  	MaxConnsPerHost int
    71  }
    72  
    73  // cache stores cache values for the RPC client methods.
    74  type cache struct {
    75  	initDone          bool
    76  	network           netmode.Magic
    77  	stateRootInHeader bool
    78  	nativeHashes      map[string]util.Uint160
    79  }
    80  
    81  // New returns a new Client ready to use. You should call Init method to
    82  // initialize stateroot setting for the network the client is operating on if
    83  // you plan using GetBlock*.
    84  func New(ctx context.Context, endpoint string, opts Options) (*Client, error) {
    85  	cl := new(Client)
    86  	err := initClient(ctx, cl, endpoint, opts)
    87  	if err != nil {
    88  		return nil, err
    89  	}
    90  	return cl, nil
    91  }
    92  
    93  func initClient(ctx context.Context, cl *Client, endpoint string, opts Options) error {
    94  	url, err := url.Parse(endpoint)
    95  	if err != nil {
    96  		return err
    97  	}
    98  
    99  	if opts.DialTimeout <= 0 {
   100  		opts.DialTimeout = defaultDialTimeout
   101  	}
   102  
   103  	if opts.RequestTimeout <= 0 {
   104  		opts.RequestTimeout = defaultRequestTimeout
   105  	}
   106  
   107  	httpClient := &http.Client{
   108  		Transport: &http.Transport{
   109  			DialContext: (&net.Dialer{
   110  				Timeout: opts.DialTimeout,
   111  			}).DialContext,
   112  			MaxConnsPerHost: opts.MaxConnsPerHost,
   113  		},
   114  		Timeout: opts.RequestTimeout,
   115  	}
   116  
   117  	// TODO(@antdm): Enable SSL.
   118  	//	if opts.Cert != "" && opts.Key != "" {
   119  	//	}
   120  
   121  	cancelCtx, cancel := context.WithCancel(ctx)
   122  	cl.ctx = cancelCtx
   123  	cl.ctxCancel = cancel
   124  	cl.cli = httpClient
   125  	cl.endpoint = url
   126  	cl.cache = cache{
   127  		nativeHashes: make(map[string]util.Uint160),
   128  	}
   129  	cl.latestReqID = atomic.Uint64{}
   130  	cl.getNextRequestID = (cl).getRequestID
   131  	cl.opts = opts
   132  	cl.requestF = cl.makeHTTPRequest
   133  	cl.reader = invoker.New(cl, nil)
   134  	return nil
   135  }
   136  
   137  func (c *Client) getRequestID() uint64 {
   138  	return c.latestReqID.Add(1)
   139  }
   140  
   141  // Init sets magic of the network client connected to, stateRootInHeader option
   142  // and native NEO, GAS and Policy contracts scripthashes. This method should be
   143  // called before any header- or block-related requests in order to deserialize
   144  // responses properly.
   145  func (c *Client) Init() error {
   146  	version, err := c.GetVersion()
   147  	if err != nil {
   148  		return fmt.Errorf("failed to get network magic: %w", err)
   149  	}
   150  	natives, err := c.GetNativeContracts()
   151  	if err != nil {
   152  		return fmt.Errorf("failed to get native contracts: %w", err)
   153  	}
   154  
   155  	c.cacheLock.Lock()
   156  	defer c.cacheLock.Unlock()
   157  
   158  	c.cache.network = version.Protocol.Network
   159  	c.cache.stateRootInHeader = version.Protocol.StateRootInHeader
   160  	for _, ctr := range natives {
   161  		c.cache.nativeHashes[ctr.Manifest.Name] = ctr.Hash
   162  	}
   163  
   164  	c.cache.initDone = true
   165  	return nil
   166  }
   167  
   168  // Close closes unused underlying networks connections.
   169  func (c *Client) Close() {
   170  	c.ctxCancel()
   171  	c.cli.CloseIdleConnections()
   172  }
   173  
   174  func (c *Client) performRequest(method string, p []any, v any) error {
   175  	if p == nil {
   176  		p = []any{} // neo-project/neo-modules#742
   177  	}
   178  	var r = neorpc.Request{
   179  		JSONRPC: neorpc.JSONRPCVersion,
   180  		Method:  method,
   181  		Params:  p,
   182  		ID:      c.getNextRequestID(),
   183  	}
   184  
   185  	raw, err := c.requestF(&r)
   186  
   187  	if raw != nil && raw.Error != nil {
   188  		return raw.Error
   189  	} else if err != nil {
   190  		return err
   191  	} else if raw == nil || raw.Result == nil {
   192  		return errors.New("no result returned")
   193  	}
   194  	return json.Unmarshal(raw.Result, v)
   195  }
   196  
   197  func (c *Client) makeHTTPRequest(r *neorpc.Request) (*neorpc.Response, error) {
   198  	var (
   199  		buf = new(bytes.Buffer)
   200  		raw = new(neorpc.Response)
   201  	)
   202  
   203  	if err := json.NewEncoder(buf).Encode(r); err != nil {
   204  		return nil, err
   205  	}
   206  
   207  	req, err := http.NewRequest("POST", c.endpoint.String(), buf)
   208  	if err != nil {
   209  		return nil, err
   210  	}
   211  	resp, err := c.cli.Do(req)
   212  	if err != nil {
   213  		return nil, err
   214  	}
   215  	defer resp.Body.Close()
   216  
   217  	// The node might send us a proper JSON anyway, so look there first and if
   218  	// it parses, it has more relevant data than HTTP error code.
   219  	err = json.NewDecoder(resp.Body).Decode(raw)
   220  	if err != nil {
   221  		if resp.StatusCode != http.StatusOK {
   222  			err = fmt.Errorf("HTTP %d/%s", resp.StatusCode, http.StatusText(resp.StatusCode))
   223  		} else {
   224  			err = fmt.Errorf("JSON decoding: %w", err)
   225  		}
   226  	}
   227  	if err != nil {
   228  		return nil, err
   229  	}
   230  	return raw, nil
   231  }
   232  
   233  // Ping attempts to create a connection to the endpoint
   234  // and returns an error if there is any.
   235  func (c *Client) Ping() error {
   236  	conn, err := net.DialTimeout("tcp", c.endpoint.Host, defaultDialTimeout)
   237  	if err != nil {
   238  		return err
   239  	}
   240  	_ = conn.Close()
   241  	return nil
   242  }
   243  
   244  // Context returns client instance context.
   245  func (c *Client) Context() context.Context {
   246  	return c.ctx
   247  }
   248  
   249  // Endpoint returns the client endpoint.
   250  func (c *Client) Endpoint() string {
   251  	return c.endpoint.String()
   252  }