github.com/status-im/status-go@v1.1.0/rpc/client.go (about)

     1  package rpc
     2  
     3  import (
     4  	"context"
     5  	"database/sql"
     6  	"encoding/base64"
     7  	"encoding/json"
     8  	"errors"
     9  	"fmt"
    10  	"net/http"
    11  	"net/url"
    12  	"reflect"
    13  	"strings"
    14  	"sync"
    15  	"time"
    16  
    17  	"github.com/ethereum/go-ethereum/ethclient"
    18  	"github.com/ethereum/go-ethereum/log"
    19  	gethrpc "github.com/ethereum/go-ethereum/rpc"
    20  
    21  	appCommon "github.com/status-im/status-go/common"
    22  	"github.com/status-im/status-go/params"
    23  	"github.com/status-im/status-go/rpc/chain"
    24  	"github.com/status-im/status-go/rpc/network"
    25  	"github.com/status-im/status-go/services/rpcstats"
    26  	"github.com/status-im/status-go/services/wallet/common"
    27  )
    28  
    29  const (
    30  	// DefaultCallTimeout is a default timeout for an RPC call
    31  	DefaultCallTimeout = time.Minute
    32  
    33  	// Names of providers
    34  	providerGrove       = "grove"
    35  	providerInfura      = "infura"
    36  	ProviderStatusProxy = "status-proxy"
    37  
    38  	mobile  = "mobile"
    39  	desktop = "desktop"
    40  
    41  	// rpcUserAgentFormat 'procurator': *an agent representing others*, aka a "proxy"
    42  	// allows for the rpc client to have a dedicated user agent, which is useful for the proxy server logs.
    43  	rpcUserAgentFormat = "procuratee-%s/%s"
    44  
    45  	// rpcUserAgentUpstreamFormat a separate user agent format for upstream, because we should not be using upstream
    46  	// if we see this user agent in the logs that means parts of the application are using a malconfigured http client
    47  	rpcUserAgentUpstreamFormat = "procuratee-%s-upstream/%s"
    48  )
    49  
    50  // List of RPC client errors.
    51  var (
    52  	ErrMethodNotFound = fmt.Errorf("the method does not exist/is not available")
    53  )
    54  
    55  var (
    56  	// rpcUserAgentName the user agent
    57  	rpcUserAgentName         = fmt.Sprintf(rpcUserAgentFormat, "no-GOOS", params.Version)
    58  	rpcUserAgentUpstreamName = fmt.Sprintf(rpcUserAgentUpstreamFormat, "no-GOOS", params.Version)
    59  )
    60  
    61  func init() {
    62  	if appCommon.IsMobilePlatform() {
    63  		rpcUserAgentName = fmt.Sprintf(rpcUserAgentFormat, mobile, params.Version)
    64  		rpcUserAgentUpstreamName = fmt.Sprintf(rpcUserAgentUpstreamFormat, mobile, params.Version)
    65  	} else {
    66  		rpcUserAgentName = fmt.Sprintf(rpcUserAgentFormat, desktop, params.Version)
    67  		rpcUserAgentUpstreamName = fmt.Sprintf(rpcUserAgentUpstreamFormat, desktop, params.Version)
    68  	}
    69  }
    70  
    71  // Handler defines handler for RPC methods.
    72  type Handler func(context.Context, uint64, ...interface{}) (interface{}, error)
    73  
    74  type ClientInterface interface {
    75  	AbstractEthClient(chainID common.ChainID) (chain.BatchCallClient, error)
    76  	EthClient(chainID uint64) (chain.ClientInterface, error)
    77  	EthClients(chainIDs []uint64) (map[uint64]chain.ClientInterface, error)
    78  	CallContext(context context.Context, result interface{}, chainID uint64, method string, args ...interface{}) error
    79  }
    80  
    81  // Client represents RPC client with custom routing
    82  // scheme. It automatically decides where RPC call
    83  // goes - Upstream or Local node.
    84  type Client struct {
    85  	sync.RWMutex
    86  
    87  	upstreamEnabled bool
    88  	upstreamURL     string
    89  	UpstreamChainID uint64
    90  
    91  	local              *gethrpc.Client
    92  	upstream           chain.ClientInterface
    93  	rpcClientsMutex    sync.RWMutex
    94  	rpcClients         map[uint64]chain.ClientInterface
    95  	rpsLimiterMutex    sync.RWMutex
    96  	limiterPerProvider map[string]*chain.RPCRpsLimiter
    97  
    98  	router         *router
    99  	NetworkManager *network.Manager
   100  
   101  	handlersMx sync.RWMutex       // mx guards handlers
   102  	handlers   map[string]Handler // locally registered handlers
   103  	log        log.Logger
   104  
   105  	walletNotifier  func(chainID uint64, message string)
   106  	providerConfigs []params.ProviderConfig
   107  }
   108  
   109  // Is initialized in a build-tag-dependent module
   110  var verifProxyInitFn func(c *Client)
   111  
   112  // NewClient initializes Client and tries to connect to both,
   113  // upstream and local node.
   114  //
   115  // Client is safe for concurrent use and will automatically
   116  // reconnect to the server if connection is lost.
   117  func NewClient(client *gethrpc.Client, upstreamChainID uint64, upstream params.UpstreamRPCConfig, networks []params.Network, db *sql.DB, providerConfigs []params.ProviderConfig) (*Client, error) {
   118  	var err error
   119  
   120  	log := log.New("package", "status-go/rpc.Client")
   121  	networkManager := network.NewManager(db)
   122  	if networkManager == nil {
   123  		return nil, errors.New("failed to create network manager")
   124  	}
   125  
   126  	err = networkManager.Init(networks)
   127  	if err != nil {
   128  		log.Error("Network manager failed to initialize", "error", err)
   129  	}
   130  
   131  	c := Client{
   132  		local:              client,
   133  		NetworkManager:     networkManager,
   134  		handlers:           make(map[string]Handler),
   135  		rpcClients:         make(map[uint64]chain.ClientInterface),
   136  		limiterPerProvider: make(map[string]*chain.RPCRpsLimiter),
   137  		log:                log,
   138  		providerConfigs:    providerConfigs,
   139  	}
   140  
   141  	var opts []gethrpc.ClientOption
   142  	opts = append(opts,
   143  		gethrpc.WithHeaders(http.Header{
   144  			"User-Agent": {rpcUserAgentUpstreamName},
   145  		}),
   146  	)
   147  
   148  	if upstream.Enabled {
   149  		c.UpstreamChainID = upstreamChainID
   150  		c.upstreamEnabled = upstream.Enabled
   151  		c.upstreamURL = upstream.URL
   152  		upstreamClient, err := gethrpc.DialOptions(context.Background(), c.upstreamURL, opts...)
   153  		if err != nil {
   154  			return nil, fmt.Errorf("dial upstream server: %s", err)
   155  		}
   156  		limiter, err := c.getRPCRpsLimiter(c.upstreamURL)
   157  		if err != nil {
   158  			return nil, fmt.Errorf("get RPC limiter: %s", err)
   159  		}
   160  		hostPortUpstream, err := extractHostFromURL(c.upstreamURL)
   161  		if err != nil {
   162  			hostPortUpstream = "upstream"
   163  		}
   164  
   165  		// Include the chain-id in the rpc client
   166  		rpcName := fmt.Sprintf("%s-chain-id-%d", hostPortUpstream, upstreamChainID)
   167  
   168  		c.upstream = chain.NewSimpleClient(*chain.NewEthClient(ethclient.NewClient(upstreamClient), limiter, upstreamClient, rpcName), upstreamChainID)
   169  	}
   170  
   171  	c.router = newRouter(c.upstreamEnabled)
   172  
   173  	if verifProxyInitFn != nil {
   174  		verifProxyInitFn(&c)
   175  	}
   176  
   177  	return &c, nil
   178  }
   179  
   180  func (c *Client) SetWalletNotifier(notifier func(chainID uint64, message string)) {
   181  	c.walletNotifier = notifier
   182  }
   183  
   184  func extractHostFromURL(inputURL string) (string, error) {
   185  	parsedURL, err := url.Parse(inputURL)
   186  	if err != nil {
   187  		return "", err
   188  	}
   189  
   190  	return parsedURL.Host, nil
   191  }
   192  
   193  func (c *Client) getRPCRpsLimiter(key string) (*chain.RPCRpsLimiter, error) {
   194  	c.rpsLimiterMutex.Lock()
   195  	defer c.rpsLimiterMutex.Unlock()
   196  	if limiter, ok := c.limiterPerProvider[key]; ok {
   197  		return limiter, nil
   198  	}
   199  	limiter := chain.NewRPCRpsLimiter()
   200  	c.limiterPerProvider[key] = limiter
   201  	return limiter, nil
   202  }
   203  
   204  func getProviderConfig(providerConfigs []params.ProviderConfig, providerName string) (params.ProviderConfig, error) {
   205  	for _, providerConfig := range providerConfigs {
   206  		if providerConfig.Name == providerName {
   207  			return providerConfig, nil
   208  		}
   209  	}
   210  	return params.ProviderConfig{}, fmt.Errorf("provider config not found for provider: %s", providerName)
   211  }
   212  
   213  func (c *Client) getClientUsingCache(chainID uint64) (chain.ClientInterface, error) {
   214  	c.rpcClientsMutex.Lock()
   215  	defer c.rpcClientsMutex.Unlock()
   216  	if rpcClient, ok := c.rpcClients[chainID]; ok {
   217  		if rpcClient.GetWalletNotifier() == nil {
   218  			rpcClient.SetWalletNotifier(c.walletNotifier)
   219  		}
   220  		return rpcClient, nil
   221  	}
   222  
   223  	network := c.NetworkManager.Find(chainID)
   224  	if network == nil {
   225  		if c.UpstreamChainID == chainID {
   226  			return c.upstream, nil
   227  		}
   228  		return nil, fmt.Errorf("could not find network: %d", chainID)
   229  	}
   230  
   231  	ethClients := c.getEthClients(network)
   232  	if len(ethClients) == 0 {
   233  		return nil, fmt.Errorf("could not find any RPC URL for chain: %d", chainID)
   234  	}
   235  
   236  	client := chain.NewClient(ethClients, chainID)
   237  	client.WalletNotifier = c.walletNotifier
   238  	c.rpcClients[chainID] = client
   239  	return client, nil
   240  }
   241  
   242  func (c *Client) getEthClients(network *params.Network) []*chain.EthClient {
   243  	urls := make(map[string]string)
   244  	keys := make([]string, 0)
   245  	authMap := make(map[string]string)
   246  
   247  	// find proxy provider
   248  	proxyProvider, err := getProviderConfig(c.providerConfigs, ProviderStatusProxy)
   249  	if err != nil {
   250  		c.log.Warn("could not find provider config for status-proxy", "error", err)
   251  	}
   252  
   253  	if proxyProvider.Enabled {
   254  		key := ProviderStatusProxy
   255  		keyFallback := ProviderStatusProxy + "-fallback"
   256  		urls[key] = network.DefaultRPCURL
   257  		urls[keyFallback] = network.DefaultFallbackURL
   258  		keys = []string{key, keyFallback}
   259  		authMap[key] = proxyProvider.User + ":" + proxyProvider.Password
   260  		authMap[keyFallback] = authMap[key]
   261  	}
   262  	keys = append(keys, []string{"main", "fallback"}...)
   263  	urls["main"] = network.RPCURL
   264  	urls["fallback"] = network.FallbackURL
   265  
   266  	ethClients := make([]*chain.EthClient, 0)
   267  	for index, key := range keys {
   268  		var rpcClient *gethrpc.Client
   269  		var rpcLimiter *chain.RPCRpsLimiter
   270  		var err error
   271  		var hostPort string
   272  		url := urls[key]
   273  
   274  		if len(url) > 0 {
   275  			// For now, we only support auth for status-proxy.
   276  			authStr, ok := authMap[key]
   277  			var opts []gethrpc.ClientOption
   278  			if ok {
   279  				authEncoded := base64.StdEncoding.EncodeToString([]byte(authStr))
   280  				opts = append(opts,
   281  					gethrpc.WithHeaders(http.Header{
   282  						"Authorization": {"Basic " + authEncoded},
   283  						"User-Agent":    {rpcUserAgentName},
   284  					}),
   285  				)
   286  			}
   287  
   288  			rpcClient, err = gethrpc.DialOptions(context.Background(), url, opts...)
   289  			if err != nil {
   290  				c.log.Error("dial server "+key, "error", err)
   291  			}
   292  
   293  			// If using the status-proxy, consider each endpoint as a separate provider
   294  			circuitKey := fmt.Sprintf("%s-%d", key, index)
   295  			// Otherwise host is good enough
   296  			if !strings.Contains(url, "status.im") {
   297  				hostPort, err = extractHostFromURL(url)
   298  				if err == nil {
   299  					circuitKey = hostPort
   300  				}
   301  			}
   302  
   303  			rpcLimiter, err = c.getRPCRpsLimiter(circuitKey)
   304  			if err != nil {
   305  				c.log.Error("get RPC limiter "+key, "error", err)
   306  			}
   307  
   308  			ethClients = append(ethClients, chain.NewEthClient(ethclient.NewClient(rpcClient), rpcLimiter, rpcClient, circuitKey))
   309  		}
   310  	}
   311  
   312  	return ethClients
   313  }
   314  
   315  // EthClient returns ethclient.Client per chain
   316  func (c *Client) EthClient(chainID uint64) (chain.ClientInterface, error) {
   317  	client, err := c.getClientUsingCache(chainID)
   318  	if err != nil {
   319  		return nil, err
   320  	}
   321  
   322  	return client, nil
   323  }
   324  
   325  // AbstractEthClient returns a partial abstraction used by new components for testing purposes
   326  func (c *Client) AbstractEthClient(chainID common.ChainID) (chain.BatchCallClient, error) {
   327  	client, err := c.getClientUsingCache(uint64(chainID))
   328  	if err != nil {
   329  		return nil, err
   330  	}
   331  
   332  	return client, nil
   333  }
   334  
   335  func (c *Client) EthClients(chainIDs []uint64) (map[uint64]chain.ClientInterface, error) {
   336  	clients := make(map[uint64]chain.ClientInterface, 0)
   337  	for _, chainID := range chainIDs {
   338  		client, err := c.getClientUsingCache(chainID)
   339  		if err != nil {
   340  			return nil, err
   341  		}
   342  		clients[chainID] = client
   343  	}
   344  
   345  	return clients, nil
   346  }
   347  
   348  // SetClient strictly for testing purposes
   349  func (c *Client) SetClient(chainID uint64, client chain.ClientInterface) {
   350  	c.rpcClientsMutex.Lock()
   351  	defer c.rpcClientsMutex.Unlock()
   352  	c.rpcClients[chainID] = client
   353  }
   354  
   355  // UpdateUpstreamURL changes the upstream RPC client URL, if the upstream is enabled.
   356  func (c *Client) UpdateUpstreamURL(url string) error {
   357  	if c.upstream == nil {
   358  		return nil
   359  	}
   360  
   361  	rpcClient, err := gethrpc.Dial(url)
   362  	if err != nil {
   363  		return err
   364  	}
   365  	rpsLimiter, err := c.getRPCRpsLimiter(url)
   366  	if err != nil {
   367  		return err
   368  	}
   369  	c.Lock()
   370  	hostPortUpstream, err := extractHostFromURL(url)
   371  	if err != nil {
   372  		hostPortUpstream = "upstream"
   373  	}
   374  	c.upstream = chain.NewSimpleClient(*chain.NewEthClient(ethclient.NewClient(rpcClient), rpsLimiter, rpcClient, hostPortUpstream), c.UpstreamChainID)
   375  	c.upstreamURL = url
   376  	c.Unlock()
   377  
   378  	return nil
   379  }
   380  
   381  // Call performs a JSON-RPC call with the given arguments and unmarshals into
   382  // result if no error occurred.
   383  //
   384  // The result must be a pointer so that package json can unmarshal into it. You
   385  // can also pass nil, in which case the result is ignored.
   386  //
   387  // It uses custom routing scheme for calls.
   388  func (c *Client) Call(result interface{}, chainID uint64, method string, args ...interface{}) error {
   389  	ctx := context.Background()
   390  	return c.CallContext(ctx, result, chainID, method, args...)
   391  }
   392  
   393  // CallContext performs a JSON-RPC call with the given arguments. If the context is
   394  // canceled before the call has successfully returned, CallContext returns immediately.
   395  //
   396  // The result must be a pointer so that package json can unmarshal into it. You
   397  // can also pass nil, in which case the result is ignored.
   398  //
   399  // It uses custom routing scheme for calls.
   400  // If there are any local handlers registered for this call, they will handle it.
   401  func (c *Client) CallContext(ctx context.Context, result interface{}, chainID uint64, method string, args ...interface{}) error {
   402  	rpcstats.CountCall(method)
   403  	if c.router.routeBlocked(method) {
   404  		return ErrMethodNotFound
   405  	}
   406  
   407  	// check locally registered handlers first
   408  	if handler, ok := c.handler(method); ok {
   409  		return c.callMethod(ctx, result, chainID, handler, args...)
   410  	}
   411  
   412  	return c.CallContextIgnoringLocalHandlers(ctx, result, chainID, method, args...)
   413  }
   414  
   415  // CallContextIgnoringLocalHandlers performs a JSON-RPC call with the given
   416  // arguments.
   417  //
   418  // If there are local handlers registered for this call, they would
   419  // be ignored. It is useful if the call is happening from within a local
   420  // handler itself.
   421  // Upstream calls routing will be used anyway.
   422  func (c *Client) CallContextIgnoringLocalHandlers(ctx context.Context, result interface{}, chainID uint64, method string, args ...interface{}) error {
   423  	if c.router.routeBlocked(method) {
   424  		return ErrMethodNotFound
   425  	}
   426  
   427  	if c.router.routeRemote(method) {
   428  		client, err := c.getClientUsingCache(chainID)
   429  		if err != nil {
   430  			return err
   431  		}
   432  		return client.CallContext(ctx, result, method, args...)
   433  	}
   434  
   435  	if c.local == nil {
   436  		c.log.Warn("Local JSON-RPC endpoint missing", "method", method)
   437  		return errors.New("missing local JSON-RPC endpoint")
   438  	}
   439  	return c.local.CallContext(ctx, result, method, args...)
   440  }
   441  
   442  // RegisterHandler registers local handler for specific RPC method.
   443  //
   444  // If method is registered, it will be executed with given handler and
   445  // never routed to the upstream or local servers.
   446  func (c *Client) RegisterHandler(method string, handler Handler) {
   447  	c.handlersMx.Lock()
   448  	defer c.handlersMx.Unlock()
   449  
   450  	c.handlers[method] = handler
   451  }
   452  
   453  // UnregisterHandler removes a previously registered handler.
   454  func (c *Client) UnregisterHandler(method string) {
   455  	c.handlersMx.Lock()
   456  	defer c.handlersMx.Unlock()
   457  
   458  	delete(c.handlers, method)
   459  }
   460  
   461  // callMethod calls registered RPC handler with given args and pointer to result.
   462  // It handles proper params and result converting
   463  //
   464  // TODO(divan): use cancellation via context here?
   465  func (c *Client) callMethod(ctx context.Context, result interface{}, chainID uint64, handler Handler, args ...interface{}) error {
   466  	response, err := handler(ctx, chainID, args...)
   467  	if err != nil {
   468  		return err
   469  	}
   470  
   471  	// if result is nil, just ignore result -
   472  	// the same way as gethrpc.CallContext() caller would expect
   473  	if result == nil {
   474  		return nil
   475  	}
   476  
   477  	return setResultFromRPCResponse(result, response)
   478  }
   479  
   480  // handler is a concurrently safe method to get registered handler by name.
   481  func (c *Client) handler(method string) (Handler, bool) {
   482  	c.handlersMx.RLock()
   483  	defer c.handlersMx.RUnlock()
   484  	handler, ok := c.handlers[method]
   485  	return handler, ok
   486  }
   487  
   488  // setResultFromRPCResponse tries to set result value from response using reflection
   489  // as concrete types are unknown.
   490  func setResultFromRPCResponse(result, response interface{}) (err error) {
   491  	defer func() {
   492  		if r := recover(); r != nil {
   493  			err = fmt.Errorf("invalid result type: %s", r)
   494  		}
   495  	}()
   496  
   497  	responseValue := reflect.ValueOf(response)
   498  
   499  	// If it is called via CallRaw, result has type json.RawMessage and
   500  	// we should marshal the response before setting it.
   501  	// Otherwise, it is called with CallContext and result is of concrete type,
   502  	// thus we should try to set it as it is.
   503  	// If response type and result type are incorrect, an error should be returned.
   504  	// TODO(divan): add additional checks for result underlying value, if needed:
   505  	// some example: https://golang.org/src/encoding/json/decode.go#L596
   506  	switch reflect.ValueOf(result).Elem().Type() {
   507  	case reflect.TypeOf(json.RawMessage{}), reflect.TypeOf([]byte{}):
   508  		data, err := json.Marshal(response)
   509  		if err != nil {
   510  			return err
   511  		}
   512  
   513  		responseValue = reflect.ValueOf(data)
   514  	}
   515  
   516  	value := reflect.ValueOf(result).Elem()
   517  	if !value.CanSet() {
   518  		return errors.New("can't assign value to result")
   519  	}
   520  	value.Set(responseValue)
   521  
   522  	return nil
   523  }