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 := ðConn{ 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 }