decred.org/dcrdex@v1.0.5/server/asset/eth/rpcclient.go (about)

     1  // This code is available on the terms of the project LICENSE.md file,
     2  // also available online at https://blueoakcouncil.org/license/1.0.0.
     3  
     4  package eth
     5  
     6  import (
     7  	"context"
     8  	"errors"
     9  	"fmt"
    10  	"math/big"
    11  	"net"
    12  	"net/url"
    13  	"sort"
    14  	"strings"
    15  	"sync"
    16  	"time"
    17  
    18  	"decred.org/dcrdex/dex"
    19  	dexeth "decred.org/dcrdex/dex/networks/eth"
    20  	swapv0 "decred.org/dcrdex/dex/networks/eth/contracts/v0"
    21  	"github.com/ethereum/go-ethereum"
    22  	"github.com/ethereum/go-ethereum/common"
    23  	"github.com/ethereum/go-ethereum/common/hexutil"
    24  	"github.com/ethereum/go-ethereum/core/types"
    25  	"github.com/ethereum/go-ethereum/ethclient"
    26  	"github.com/ethereum/go-ethereum/rpc"
    27  )
    28  
    29  // Check that rpcclient satisfies the ethFetcher interface.
    30  var (
    31  	_ ethFetcher = (*rpcclient)(nil)
    32  
    33  	bigZero                    = new(big.Int)
    34  	headerExpirationTime       = time.Minute
    35  	monitorConnectionsInterval = 30 * time.Second
    36  	// failingEndpointsCheckFreq means that endpoints that were never connected
    37  	// will be attempted every (monitorConnectionsInterval * failingEndpointsCheckFreq).
    38  	failingEndpointsCheckFreq = 4
    39  )
    40  
    41  type ContextCaller interface {
    42  	CallContext(ctx context.Context, result any, method string, args ...any) error
    43  }
    44  
    45  type ethConn struct {
    46  	*ethclient.Client
    47  	endpoint string
    48  	priority uint16
    49  	// swapContract is the current ETH swapContract.
    50  	swapContract swapContract
    51  	// tokens are tokeners for loaded tokens. tokens is not protected by a
    52  	// mutex, as it is expected that the caller will connect and place calls to
    53  	// loadToken sequentially in the same thread during initialization.
    54  	tokens map[uint32]*tokener
    55  	// caller is a client for raw calls not implemented by *ethclient.Client.
    56  	caller          ContextCaller
    57  	txPoolSupported bool
    58  
    59  	tipCache struct {
    60  		sync.Mutex
    61  		expiration time.Duration
    62  		lastUpdate time.Time
    63  		hdr        *types.Header
    64  	}
    65  }
    66  
    67  func (ec *ethConn) String() string {
    68  	return ec.endpoint
    69  }
    70  
    71  // monitorBlocks creates a block header subscription and updates the tipCache.
    72  func (ec *ethConn) monitorBlocks(ctx context.Context, log dex.Logger) {
    73  	c := &ec.tipCache
    74  
    75  	// No matter why we exit, revert to manual tip checks.
    76  	defer func() {
    77  		log.Tracef("Exiting block monitor for %s", ec.endpoint)
    78  		c.Lock()
    79  		c.expiration = time.Second * 99 / 10 // 9.9 seconds.
    80  		c.Unlock()
    81  	}()
    82  
    83  	h := make(chan *types.Header, 8)
    84  	sub, err := ec.SubscribeNewHead(ctx, h)
    85  	if err != nil {
    86  		log.Errorf("Error connecting to Websockets headers: %w", err)
    87  		return
    88  	}
    89  
    90  	defer func() {
    91  		// If a provider does not respond to an unsubscribe request, the unsubscribe function
    92  		// will never return because geth does not use a timeout.
    93  		doneUnsubbing := make(chan struct{})
    94  		go func() {
    95  			sub.Unsubscribe()
    96  			close(doneUnsubbing)
    97  		}()
    98  		select {
    99  		case <-doneUnsubbing:
   100  		case <-time.After(10 * time.Second):
   101  			log.Errorf("Timed out waiting to unsubscribe from %q", ec.endpoint)
   102  		}
   103  	}()
   104  
   105  	for {
   106  		select {
   107  		case hdr := <-h:
   108  			c.Lock()
   109  			c.hdr = hdr
   110  			c.lastUpdate = time.Now()
   111  			c.Unlock()
   112  		case err, ok := <-sub.Err():
   113  			if !ok {
   114  				// Subscription cancelled
   115  				return
   116  			}
   117  			if ctx.Err() != nil || err == nil { // Both conditions indicate normal close
   118  				return
   119  			}
   120  			log.Errorf("Header subscription to %s failed with error: %v", ec.endpoint, err)
   121  			log.Infof("Falling back to manual header requests for %s", ec.endpoint)
   122  			return
   123  		case <-ctx.Done():
   124  			return
   125  		}
   126  	}
   127  }
   128  
   129  type endpoint struct {
   130  	url      string
   131  	priority uint16
   132  }
   133  
   134  func (ec *ethConn) tip(ctx context.Context) (*types.Header, error) {
   135  	cache := &ec.tipCache
   136  	cache.Lock()
   137  	defer cache.Unlock()
   138  	if time.Since(cache.lastUpdate) < cache.expiration && cache.hdr != nil {
   139  		return cache.hdr, nil
   140  	}
   141  	hdr, err := ec.HeaderByNumber(ctx, nil)
   142  	if err != nil {
   143  		return nil, err
   144  	}
   145  	cache.lastUpdate = time.Now()
   146  	cache.hdr = hdr
   147  	return hdr, nil
   148  }
   149  
   150  func (ep endpoint) String() string {
   151  	return ep.url
   152  }
   153  
   154  var _ fmt.Stringer = endpoint{} // compile error if pointer receiver
   155  var _ fmt.Stringer = (*endpoint)(nil)
   156  
   157  type rpcclient struct {
   158  	net dex.Network
   159  	log dex.Logger
   160  
   161  	baseChainID    uint32
   162  	genesisChainID uint64
   163  	baseChainName  string
   164  
   165  	// endpoints should only be used during connect to know which endpoints
   166  	// to attempt to connect. If we were unable to connect to some of the
   167  	// endpoints, they will not be included in the clients slice.
   168  	endpoints []endpoint
   169  	// neverConnectedEndpoints failed to connect since the initial connect call,
   170  	// so an ethConn has not been created for them.
   171  	neverConnectedEndpoints []endpoint
   172  	healthCheckCounter      int
   173  	tokensLoaded            map[uint32]*VersionedToken
   174  	ethContractAddr         common.Address
   175  
   176  	// the order of clients will change based on the health of the connections.
   177  	clientsMtx sync.RWMutex
   178  	clients    []*ethConn
   179  }
   180  
   181  func newRPCClient(baseChainID uint32, chainID uint64, net dex.Network, endpoints []endpoint, ethContractAddr common.Address, log dex.Logger) *rpcclient {
   182  	return &rpcclient{
   183  		baseChainID:     baseChainID,
   184  		genesisChainID:  chainID,
   185  		baseChainName:   strings.ToUpper(dex.BipIDSymbol(baseChainID)),
   186  		net:             net,
   187  		endpoints:       endpoints,
   188  		log:             log,
   189  		ethContractAddr: ethContractAddr,
   190  		tokensLoaded:    make(map[uint32]*VersionedToken),
   191  	}
   192  }
   193  
   194  func (c *rpcclient) clientsCopy() []*ethConn {
   195  	c.clientsMtx.RLock()
   196  	defer c.clientsMtx.RUnlock()
   197  
   198  	clients := make([]*ethConn, len(c.clients))
   199  	copy(clients, c.clients)
   200  	return clients
   201  }
   202  
   203  func (c *rpcclient) connectToEndpoint(ctx context.Context, endpoint endpoint) (*ethConn, error) {
   204  	var success bool
   205  
   206  	client, err := rpc.DialContext(ctx, endpoint.url)
   207  	if err != nil {
   208  		return nil, err
   209  	}
   210  
   211  	defer func() {
   212  		// This shouldn't happen as the only possible errors are due to ETHSwap and
   213  		// tokener creation.
   214  		if !success {
   215  			client.Close()
   216  		}
   217  	}()
   218  
   219  	ec := &ethConn{
   220  		Client:   ethclient.NewClient(client),
   221  		endpoint: endpoint.url,
   222  		priority: endpoint.priority,
   223  		tokens:   make(map[uint32]*tokener),
   224  		caller:   client,
   225  	}
   226  
   227  	chainID, err := ec.ChainID(ctx)
   228  	if err != nil {
   229  		return nil, fmt.Errorf("error checking chain ID from %q: %w", endpoint.url, err)
   230  	}
   231  	if chainID.Uint64() != c.genesisChainID {
   232  		return nil, fmt.Errorf("wrong chain ID from %q. wanted %d, got %d", endpoint.url, c.genesisChainID, chainID)
   233  	}
   234  
   235  	// ETHBackend will check rpcclient.blockNumber() once per second. For
   236  	// external http sources, that's an excessive request rate.
   237  	uri, _ := url.Parse(endpoint.url) // err already checked by DialContext
   238  	isWS := uri.Scheme == "ws" || uri.Scheme == "wss"
   239  	// High block tip check frequency for local http.
   240  	ec.tipCache.expiration = time.Second * 9 / 10
   241  	// Websocket endpoints receive headers through a notification feed, so
   242  	// shouldn't make requests unless something seems wrong.
   243  	if isWS {
   244  		ec.tipCache.expiration = headerExpirationTime // time.Minute
   245  	} else if isRemoteURL(uri) {
   246  		// Lower the request rate for non-loopback IPs to avoid running into
   247  		// rate limits.
   248  		ec.tipCache.expiration = time.Second * 99 / 10
   249  	}
   250  
   251  	reqModules := []string{"eth", "txpool"}
   252  	if err := dexeth.CheckAPIModules(client, endpoint.url, c.log, reqModules); err != nil {
   253  		c.log.Warnf("Error checking required modules at %q: %v", endpoint, err)
   254  		c.log.Warnf("Will not account for pending transactions in balance calculations at %q", endpoint)
   255  		ec.txPoolSupported = false
   256  	} else {
   257  		ec.txPoolSupported = true
   258  	}
   259  
   260  	es, err := swapv0.NewETHSwap(c.ethContractAddr, ec.Client)
   261  	if err != nil {
   262  		return nil, fmt.Errorf("unable to initialize %v contract for %q: %v", c.baseChainName, endpoint, err)
   263  	}
   264  	ec.swapContract = &swapSourceV0{es}
   265  
   266  	for assetID, vToken := range c.tokensLoaded {
   267  		tkn, err := newTokener(ctx, vToken, c.net, ec.Client)
   268  		if err != nil {
   269  			return nil, fmt.Errorf("error constructing ERC20Swap: %w", err)
   270  		}
   271  		ec.tokens[assetID] = tkn
   272  	}
   273  
   274  	if isWS {
   275  		go ec.monitorBlocks(ctx, c.log)
   276  	}
   277  
   278  	success = true
   279  
   280  	return ec, nil
   281  }
   282  
   283  type connectionStatus int
   284  
   285  const (
   286  	connectionStatusFailed connectionStatus = iota
   287  	connectionStatusOutdated
   288  	connectionStatusConnected
   289  )
   290  
   291  func (c *rpcclient) checkConnectionStatus(ctx context.Context, ec *ethConn) connectionStatus {
   292  	hdr, err := ec.tip(ctx)
   293  	if err != nil {
   294  		c.log.Errorf("Failed to get header from %q: %v", ec.endpoint, err)
   295  		return connectionStatusFailed
   296  	}
   297  
   298  	if c.headerIsOutdated(hdr) {
   299  		hdrTime := time.Unix(int64(hdr.Time), 0)
   300  		c.log.Warnf("header fetched from %q appears to be outdated (time %s is %v old). "+
   301  			"If you continue to see this message, you might need to check your system clock",
   302  			ec.endpoint, hdrTime, time.Since(hdrTime))
   303  		return connectionStatusOutdated
   304  	}
   305  
   306  	return connectionStatusConnected
   307  }
   308  
   309  // sortConnectionsByHealth checks the health of the connections and sorts them
   310  // based on their health. It does a best header call to each connection and
   311  // connections with non outdated headers are placed first, ones with outdated
   312  // headers are placed in the middle, and ones that error are placed last.
   313  // Every failingEndpointsCheckFreq health checks, the endpoints that have
   314  // never been successfully connection will be checked. True is returned if
   315  // there is at least one healthy connection.
   316  func (c *rpcclient) sortConnectionsByHealth(ctx context.Context) bool {
   317  	clients := c.clientsCopy()
   318  
   319  	healthyConnections := make([]*ethConn, 0, len(clients))
   320  	outdatedConnections := make([]*ethConn, 0, len(clients))
   321  	failingConnections := make([]*ethConn, 0, len(clients))
   322  
   323  	categorizeConnection := func(conn *ethConn) {
   324  		status := c.checkConnectionStatus(ctx, conn)
   325  		switch status {
   326  		case connectionStatusConnected:
   327  			healthyConnections = append(healthyConnections, conn)
   328  		case connectionStatusOutdated:
   329  			outdatedConnections = append(outdatedConnections, conn)
   330  		case connectionStatusFailed:
   331  			failingConnections = append(failingConnections, conn)
   332  		}
   333  	}
   334  
   335  	for _, ec := range clients {
   336  		categorizeConnection(ec)
   337  	}
   338  
   339  	if c.healthCheckCounter == 0 && len(c.neverConnectedEndpoints) > 0 {
   340  		stillUnconnectedEndpoints := make([]endpoint, 0, len(c.neverConnectedEndpoints))
   341  
   342  		for _, endpoint := range c.neverConnectedEndpoints {
   343  			ec, err := c.connectToEndpoint(ctx, endpoint)
   344  			if err != nil {
   345  				c.log.Errorf("Error connecting to %q: %v", endpoint, err)
   346  				stillUnconnectedEndpoints = append(stillUnconnectedEndpoints, endpoint)
   347  				continue
   348  			}
   349  
   350  			c.log.Infof("Successfully connected to %q", endpoint)
   351  
   352  			categorizeConnection(ec)
   353  		}
   354  
   355  		c.neverConnectedEndpoints = stillUnconnectedEndpoints
   356  	}
   357  
   358  	// Higher priority comes first.
   359  	sort.Slice(healthyConnections, func(i, j int) bool {
   360  		return healthyConnections[i].priority > healthyConnections[j].priority
   361  	})
   362  	sort.Slice(outdatedConnections, func(i, j int) bool {
   363  		return outdatedConnections[i].priority > outdatedConnections[j].priority
   364  	})
   365  	sort.Slice(failingConnections, func(i, j int) bool {
   366  		return failingConnections[i].priority > failingConnections[j].priority
   367  	})
   368  
   369  	clientsUpdatedOrder := make([]*ethConn, 0, len(clients))
   370  	clientsUpdatedOrder = append(clientsUpdatedOrder, healthyConnections...)
   371  	clientsUpdatedOrder = append(clientsUpdatedOrder, outdatedConnections...)
   372  	clientsUpdatedOrder = append(clientsUpdatedOrder, failingConnections...)
   373  
   374  	c.log.Tracef("Healthy connections: %v", healthyConnections)
   375  	if len(outdatedConnections) > 0 {
   376  		c.log.Warnf("Outdated connections: %v", outdatedConnections)
   377  	}
   378  	if len(failingConnections) > 0 {
   379  		c.log.Warnf("Failing connections: %v", failingConnections)
   380  	}
   381  
   382  	c.clientsMtx.Lock()
   383  	defer c.clientsMtx.Unlock()
   384  	c.clients = clientsUpdatedOrder
   385  	c.healthCheckCounter = (c.healthCheckCounter + 1) % failingEndpointsCheckFreq
   386  
   387  	return len(healthyConnections) > 0
   388  }
   389  
   390  // markConnectionAsFailed moves an connection to the end of the client list.
   391  func (c *rpcclient) markConnectionAsFailed(endpoint string) {
   392  	c.clientsMtx.Lock()
   393  	defer c.clientsMtx.Unlock()
   394  
   395  	var index int = -1
   396  	for i, ec := range c.clients {
   397  		if ec.endpoint == endpoint {
   398  			index = i
   399  			break
   400  		}
   401  	}
   402  	if index == -1 {
   403  		c.log.Errorf("Failed to mark client as failed: %q not found", endpoint)
   404  		return
   405  	}
   406  
   407  	updatedClients := make([]*ethConn, 0, len(c.clients))
   408  	updatedClients = append(updatedClients, c.clients[:index]...)
   409  	updatedClients = append(updatedClients, c.clients[index+1:]...)
   410  	updatedClients = append(updatedClients, c.clients[index])
   411  
   412  	c.clients = updatedClients
   413  }
   414  
   415  // monitorConnectionsHealth starts a goroutine that checks the health of all
   416  // connections every 30 seconds.
   417  func (c *rpcclient) monitorConnectionsHealth(ctx context.Context) {
   418  	defer func() {
   419  		for _, ec := range c.clientsCopy() {
   420  			ec.Close()
   421  		}
   422  	}()
   423  
   424  	ticker := time.NewTicker(monitorConnectionsInterval)
   425  	defer ticker.Stop()
   426  
   427  	for {
   428  		select {
   429  		case <-ctx.Done():
   430  			return
   431  		case <-ticker.C:
   432  			if !c.sortConnectionsByHealth(ctx) {
   433  				c.log.Warnf("No healthy %v RPC connections", c.baseChainName)
   434  			}
   435  		}
   436  	}
   437  }
   438  
   439  func (c *rpcclient) withClient(f func(ec *ethConn) error, haltOnNotFound ...bool) (err error) {
   440  	for _, ec := range c.clientsCopy() {
   441  		err = f(ec)
   442  		if err == nil {
   443  			return nil
   444  		}
   445  		if len(haltOnNotFound) > 0 && haltOnNotFound[0] && (errors.Is(err, ethereum.NotFound) || strings.Contains(err.Error(), "not found")) {
   446  			return err
   447  		}
   448  
   449  		c.log.Errorf("Unpropagated error from %q: %v", ec.endpoint, err)
   450  		c.markConnectionAsFailed(ec.endpoint)
   451  	}
   452  
   453  	return fmt.Errorf("all providers failed. last error: %w", err)
   454  }
   455  
   456  // connect will attempt to connect to all the endpoints in the endpoints slice.
   457  // If at least one of the connections is successful and is not outdated, the
   458  // function will return without error.
   459  //
   460  // Connections with an outdated block will be marked as outdated, but included
   461  // in the clients slice. If the up-to-date providers start to fail, the outdated
   462  // ones will be checked to see if they are still outdated.
   463  //
   464  // Failed connections will not be included in the clients slice.
   465  func (c *rpcclient) connect(ctx context.Context) (err error) {
   466  	var success bool
   467  
   468  	c.clients = make([]*ethConn, 0, len(c.endpoints))
   469  	c.neverConnectedEndpoints = make([]endpoint, 0, len(c.endpoints))
   470  
   471  	for _, endpoint := range c.endpoints {
   472  		ec, err := c.connectToEndpoint(ctx, endpoint)
   473  		if err != nil {
   474  			c.log.Errorf("Error connecting to %q: %v", endpoint, err)
   475  			c.neverConnectedEndpoints = append(c.neverConnectedEndpoints, endpoint)
   476  			continue
   477  		}
   478  
   479  		defer func() {
   480  			// If all connections are outdated, we will not start, so close any open connections.
   481  			if !success {
   482  				ec.Close()
   483  			}
   484  		}()
   485  
   486  		c.clients = append(c.clients, ec)
   487  	}
   488  
   489  	success = c.sortConnectionsByHealth(ctx)
   490  
   491  	if !success {
   492  		return fmt.Errorf("failed to connect to an up-to-date %v node", c.baseChainName)
   493  	}
   494  
   495  	go c.monitorConnectionsHealth(ctx)
   496  
   497  	return nil
   498  }
   499  
   500  func (c *rpcclient) headerIsOutdated(hdr *types.Header) bool {
   501  	return c.net != dex.Simnet && hdr.Time < uint64(time.Now().Add(-headerExpirationTime).Unix())
   502  }
   503  
   504  func (c *rpcclient) loadToken(ctx context.Context, assetID uint32, vToken *VersionedToken) error {
   505  	c.tokensLoaded[assetID] = vToken
   506  
   507  	for _, cl := range c.clientsCopy() {
   508  		tkn, err := newTokener(ctx, vToken, c.net, cl.Client)
   509  		if err != nil {
   510  			return fmt.Errorf("error constructing ERC20Swap: %w", err)
   511  		}
   512  		cl.tokens[assetID] = tkn
   513  	}
   514  	return nil
   515  }
   516  
   517  func (c *rpcclient) withTokener(assetID uint32, f func(*tokener) error) error {
   518  	return c.withClient(func(ec *ethConn) error {
   519  		tkn, found := ec.tokens[assetID]
   520  		if !found {
   521  			return fmt.Errorf("no swap source for asset %d", assetID)
   522  		}
   523  		return f(tkn)
   524  	})
   525  
   526  }
   527  
   528  // bestHeader gets the best header at the time of calling.
   529  func (c *rpcclient) bestHeader(ctx context.Context) (hdr *types.Header, err error) {
   530  	return hdr, c.withClient(func(ec *ethConn) error {
   531  		hdr, err = ec.tip(ctx)
   532  		return err
   533  	})
   534  }
   535  
   536  // headerByHeight gets the best header at height.
   537  func (c *rpcclient) headerByHeight(ctx context.Context, height uint64) (hdr *types.Header, err error) {
   538  	return hdr, c.withClient(func(ec *ethConn) error {
   539  		hdr, err = ec.HeaderByNumber(ctx, big.NewInt(int64(height)))
   540  		return err
   541  	})
   542  }
   543  
   544  // suggestGasTipCap retrieves the currently suggested priority fee to allow a
   545  // timely execution of a transaction.
   546  func (c *rpcclient) suggestGasTipCap(ctx context.Context) (tipCap *big.Int, err error) {
   547  	return tipCap, c.withClient(func(ec *ethConn) error {
   548  		tipCap, err = ec.SuggestGasTipCap(ctx)
   549  		return err
   550  	})
   551  }
   552  
   553  // blockNumber gets the chain length at the time of calling.
   554  func (c *rpcclient) blockNumber(ctx context.Context) (bn uint64, err error) {
   555  	return bn, c.withClient(func(ec *ethConn) error {
   556  		hdr, err := ec.tip(ctx)
   557  		if err == nil {
   558  			bn = hdr.Number.Uint64()
   559  		}
   560  		return err
   561  	})
   562  }
   563  
   564  // swap gets a swap keyed by secretHash in the contract.
   565  func (c *rpcclient) swap(ctx context.Context, assetID uint32, secretHash [32]byte) (state *dexeth.SwapState, err error) {
   566  	if assetID == c.baseChainID {
   567  		return state, c.withClient(func(ec *ethConn) error {
   568  			state, err = ec.swapContract.Swap(ctx, secretHash)
   569  			return err
   570  		})
   571  	}
   572  	return state, c.withTokener(assetID, func(tkn *tokener) error {
   573  		state, err = tkn.Swap(ctx, secretHash)
   574  		return err
   575  	})
   576  }
   577  
   578  // transaction gets the transaction that hashes to hash from the chain or
   579  // mempool. Errors if tx does not exist.
   580  func (c *rpcclient) transaction(ctx context.Context, hash common.Hash) (tx *types.Transaction, isMempool bool, err error) {
   581  	return tx, isMempool, c.withClient(func(ec *ethConn) error {
   582  		tx, isMempool, err = ec.TransactionByHash(ctx, hash)
   583  		return err
   584  	}, true) // stop on first provider with "not found", because this should be an error if tx does not exist
   585  }
   586  
   587  // dumbBalance gets the account balance, ignoring the effects of unmined
   588  // transactions.
   589  func (c *rpcclient) dumbBalance(ctx context.Context, ec *ethConn, assetID uint32, addr common.Address) (bal *big.Int, err error) {
   590  	if assetID == c.baseChainID {
   591  		return ec.BalanceAt(ctx, addr, nil)
   592  	}
   593  	tkn := ec.tokens[assetID]
   594  	if tkn == nil {
   595  		return nil, fmt.Errorf("no tokener for asset ID %d", assetID)
   596  	}
   597  	return tkn.balanceOf(ctx, addr)
   598  }
   599  
   600  // smartBalance gets the account balance, including the effects of known
   601  // unmined transactions.
   602  func (c *rpcclient) smartBalance(ctx context.Context, ec *ethConn, assetID uint32, addr common.Address) (bal *big.Int, err error) {
   603  	tip, err := c.blockNumber(ctx)
   604  	if err != nil {
   605  		return nil, fmt.Errorf("blockNumber error: %v", err)
   606  	}
   607  
   608  	// We need to subtract and pending outgoing value, but ignore any pending
   609  	// incoming value since that can't be spent until mined. So we can't using
   610  	// PendingBalanceAt or BalanceAt by themselves.
   611  	// We'll iterate tx pool transactions and subtract any value and fees being
   612  	// sent from this account. The rpc.Client doesn't expose the
   613  	// txpool_contentFrom => (*TxPool).ContentFrom RPC method, for whatever
   614  	// reason, so we'll have to use CallContext and copy the mimic the
   615  	// internal RPCTransaction type.
   616  	var txs map[string]map[string]*RPCTransaction
   617  	if err := ec.caller.CallContext(ctx, &txs, "txpool_contentFrom", addr); err != nil {
   618  		return nil, fmt.Errorf("contentFrom error: %w", err)
   619  	}
   620  
   621  	if assetID == c.baseChainID {
   622  		ethBalance, err := ec.BalanceAt(ctx, addr, big.NewInt(int64(tip)))
   623  		if err != nil {
   624  			return nil, err
   625  		}
   626  		outgoingEth := new(big.Int)
   627  		for _, group := range txs { // 2 groups, pending and queued
   628  			for _, tx := range group {
   629  				outgoingEth.Add(outgoingEth, tx.Value.ToInt())
   630  				gas := new(big.Int).SetUint64(uint64(tx.Gas))
   631  				if tx.GasPrice != nil && tx.GasPrice.ToInt().Cmp(bigZero) > 0 {
   632  					outgoingEth.Add(outgoingEth, new(big.Int).Mul(gas, tx.GasPrice.ToInt()))
   633  				} else if tx.GasFeeCap != nil {
   634  					outgoingEth.Add(outgoingEth, new(big.Int).Mul(gas, tx.GasFeeCap.ToInt()))
   635  				} else {
   636  					return nil, fmt.Errorf("cannot find fees for tx %s", tx.Hash)
   637  				}
   638  			}
   639  		}
   640  		return ethBalance.Sub(ethBalance, outgoingEth), nil
   641  	}
   642  
   643  	// For tokens, we'll do something similar, but with checks for pending txs
   644  	// that transfer tokens or pay to the swap contract.
   645  	// Can't use withTokener because we need to use the same ethConn due to
   646  	// txPoolSupported being used to decide between {smart/dumb}Balance.
   647  	tkn := ec.tokens[assetID]
   648  	if tkn == nil {
   649  		return nil, fmt.Errorf("no tokener for asset ID %d", assetID)
   650  	}
   651  	bal, err = tkn.balanceOf(ctx, addr)
   652  	if err != nil {
   653  		return nil, err
   654  	}
   655  	for _, group := range txs {
   656  		for _, rpcTx := range group {
   657  			to := *rpcTx.To
   658  			if to == tkn.tokenAddr {
   659  				if sent := tkn.transferred(rpcTx.Input); sent != nil {
   660  					bal.Sub(bal, sent)
   661  				}
   662  			}
   663  			if to == tkn.contractAddr {
   664  				if swapped := tkn.swapped(rpcTx.Input); swapped != nil {
   665  					bal.Sub(bal, swapped)
   666  				}
   667  			}
   668  		}
   669  	}
   670  	return bal, nil
   671  }
   672  
   673  // accountBalance gets the account balance. If txPool functions are supported by the
   674  // client, it will include the effects of unmined transactions, otherwise it will not.
   675  func (c *rpcclient) accountBalance(ctx context.Context, assetID uint32, addr common.Address) (bal *big.Int, err error) {
   676  	return bal, c.withClient(func(ec *ethConn) error {
   677  		if ec.txPoolSupported {
   678  			bal, err = c.smartBalance(ctx, ec, assetID, addr)
   679  		} else {
   680  			bal, err = c.dumbBalance(ctx, ec, assetID, addr)
   681  		}
   682  		return err
   683  	})
   684  
   685  }
   686  
   687  type RPCTransaction struct {
   688  	Value     *hexutil.Big    `json:"value"`
   689  	Gas       hexutil.Uint64  `json:"gas"`
   690  	GasPrice  *hexutil.Big    `json:"gasPrice"`
   691  	GasFeeCap *hexutil.Big    `json:"maxFeePerGas,omitempty"`
   692  	Hash      common.Hash     `json:"hash"`
   693  	To        *common.Address `json:"to"`
   694  	Input     hexutil.Bytes   `json:"input"`
   695  	// BlockHash        *common.Hash      `json:"blockHash"`
   696  	// BlockNumber      *hexutil.Big      `json:"blockNumber"`
   697  	// From             common.Address    `json:"from"`
   698  	// GasTipCap        *hexutil.Big      `json:"maxPriorityFeePerGas,omitempty"`
   699  	// Nonce            hexutil.Uint64    `json:"nonce"`
   700  	// TransactionIndex *hexutil.Uint64   `json:"transactionIndex"`
   701  	// Type             hexutil.Uint64    `json:"type"`
   702  	// Accesses         *types.AccessList `json:"accessList,omitempty"`
   703  	// ChainID          *hexutil.Big      `json:"chainId,omitempty"`
   704  	// V                *hexutil.Big      `json:"v"`
   705  	// R                *hexutil.Big      `json:"r"`
   706  	// S                *hexutil.Big      `json:"s"`
   707  }
   708  
   709  func isRemoteURL(uri *url.URL) bool {
   710  	host := uri.Hostname()
   711  	ip := net.ParseIP(host)
   712  	if ip == nil {
   713  		ips, _ := net.LookupIP(host)
   714  		if len(ips) == 0 {
   715  			return true
   716  		}
   717  		ip = ips[0]
   718  	}
   719  	return !ip.IsLoopback()
   720  }