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 }