@@ -61,6 +61,7 @@ import (
6161 "github.com/btcsuite/btcd/chaincfg"
6262 "github.com/ethereum/go-ethereum/params"
6363 "github.com/sirupsen/logrus"
64+ "golang.org/x/time/rate"
6465)
6566
6667func init () {
@@ -225,10 +226,11 @@ type Backend struct {
225226
226227 socksProxy socksproxy.SocksProxy
227228 // can be a regular or, if Tor is enabled in the config, a SOCKS5 proxy client.
228- httpClient * http.Client
229- etherScanHTTPClient * http.Client
230- ratesUpdater * rates.RateUpdater
231- banners * banners.Banners
229+ httpClient * http.Client
230+ etherScanHTTPClient * http.Client
231+ etherScanRateLimiter * rate.Limiter
232+ ratesUpdater * rates.RateUpdater
233+ banners * banners.Banners
232234
233235 // For unit tests, called when `backend.checkAccountUsed()` is called.
234236 tstCheckAccountUsed func (accounts.Interface ) bool
@@ -240,6 +242,15 @@ type Backend struct {
240242
241243 // isOnline indicates whether the backend is online, i.e. able to connect to the internet.
242244 isOnline atomic.Bool
245+
246+ // quit is used to indicate to running goroutines that they should stop as the backend is being closed
247+ quit chan struct {}
248+
249+ // enqueueUpdateForAccount is used to enqueue an update for an account.
250+ enqueueUpdateForAccount chan * eth.Account
251+
252+ // updateETHAccountsCh is used to trigger an update of all ETH accounts.
253+ updateETHAccountsCh chan struct {}
243254}
244255
245256// NewBackend creates a new backend with the given arguments.
@@ -262,11 +273,13 @@ func NewBackend(arguments *arguments.Arguments, environment Environment) (*Backe
262273 return nil , err
263274 }
264275
276+ accountUpdate := make (chan * eth.Account )
277+
265278 backend := & Backend {
266279 arguments : arguments ,
267280 environment : environment ,
268281 config : backendConfig ,
269- events : make (chan interface {}, 1000 ),
282+ events : make (chan interface {}),
270283
271284 devices : map [string ]device.Interface {},
272285 coins : map [coinpkg.Code ]coinpkg.Coin {},
@@ -276,12 +289,15 @@ func NewBackend(arguments *arguments.Arguments, environment Environment) (*Backe
276289 return btc .NewAccount (config , coin , gapLimits , getAddress , log , hclient )
277290 },
278291 makeEthAccount : func (config * accounts.AccountConfig , coin * eth.Coin , httpClient * http.Client , log * logrus.Entry ) accounts.Interface {
279- return eth .NewAccount (config , coin , httpClient , log )
292+ return eth .NewAccount (config , coin , httpClient , log , accountUpdate )
280293 },
281294
282295 log : log ,
283296
284- testing : backendConfig .AppConfig ().Backend .StartInTestnet || arguments .Testing (),
297+ testing : backendConfig .AppConfig ().Backend .StartInTestnet || arguments .Testing (),
298+ quit : make (chan struct {}),
299+ etherScanRateLimiter : rate .NewLimiter (rate .Limit (etherscan .CallsPerSec ), 1 ),
300+ enqueueUpdateForAccount : accountUpdate ,
285301 }
286302 // TODO: remove when connectivity check is present on all platforms
287303 backend .isOnline .Store (true )
@@ -529,19 +545,19 @@ func (backend *Backend) Coin(code coinpkg.Code) (coinpkg.Coin, error) {
529545 coin = btc .NewCoin (coinpkg .CodeLTC , "Litecoin" , "LTC" , coinpkg .BtcUnitDefault , & ltc .MainNetParams , dbFolder , servers ,
530546 "https://blockchair.com/litecoin/transaction/" , backend .socksProxy )
531547 case code == coinpkg .CodeETH :
532- etherScan := etherscan .NewEtherScan ("1" , backend .etherScanHTTPClient )
548+ etherScan := etherscan .NewEtherScan ("1" , backend .etherScanHTTPClient , backend . etherScanRateLimiter )
533549 coin = eth .NewCoin (etherScan , code , "Ethereum" , "ETH" , "ETH" , params .MainnetChainConfig ,
534550 "https://etherscan.io/tx/" ,
535551 etherScan ,
536552 nil )
537553 case code == coinpkg .CodeSEPETH :
538- etherScan := etherscan .NewEtherScan ("11155111" , backend .etherScanHTTPClient )
554+ etherScan := etherscan .NewEtherScan ("11155111" , backend .etherScanHTTPClient , backend . etherScanRateLimiter )
539555 coin = eth .NewCoin (etherScan , code , "Ethereum Sepolia" , "SEPETH" , "SEPETH" , params .SepoliaChainConfig ,
540556 "https://sepolia.etherscan.io/tx/" ,
541557 etherScan ,
542558 nil )
543559 case erc20Token != nil :
544- etherScan := etherscan .NewEtherScan ("1" , backend .etherScanHTTPClient )
560+ etherScan := etherscan .NewEtherScan ("1" , backend .etherScanHTTPClient , backend . etherScanRateLimiter )
545561 coin = eth .NewCoin (etherScan , erc20Token .code , erc20Token .name , erc20Token .unit , "ETH" , params .MainnetChainConfig ,
546562 "https://etherscan.io/tx/" ,
547563 etherScan ,
@@ -555,6 +571,74 @@ func (backend *Backend) Coin(code coinpkg.Code) (coinpkg.Coin, error) {
555571 return coin , nil
556572}
557573
574+ func (backend * Backend ) pollETHAccounts () {
575+ timer := time .After (0 )
576+
577+ updateAll := func () {
578+ if err := backend .updateETHAccounts (); err != nil {
579+ backend .log .WithError (err ).Error ("could not update ETH accounts" )
580+ }
581+ }
582+
583+ for {
584+ select {
585+ case <- backend .quit :
586+ return
587+ default :
588+ select {
589+ case <- backend .quit :
590+ return
591+ case account := <- backend .enqueueUpdateForAccount :
592+ go func () {
593+ // A single ETH accounts needs an update.
594+ ethCoin , ok := account .Coin ().(* eth.Coin )
595+ if ! ok {
596+ backend .log .WithField ("account" , account .Config ().Config .Name ).Errorf ("expected ETH account to have ETH coin, got %T" , account .Coin ())
597+ }
598+ etherScanClient := etherscan .NewEtherScan (ethCoin .ChainIDstr (), backend .etherScanHTTPClient , backend .etherScanRateLimiter )
599+ if err := eth .UpdateBalances ([]* eth.Account {account }, etherScanClient ); err != nil {
600+ backend .log .WithError (err ).Errorf ("could not update account %s" , account .Config ().Config .Name )
601+ }
602+ }()
603+ case <- backend .updateETHAccountsCh :
604+ go updateAll ()
605+ timer = time .After (eth .PollInterval )
606+ case <- timer :
607+ go updateAll ()
608+ timer = time .After (eth .PollInterval )
609+ }
610+ }
611+ }
612+ }
613+
614+ func (backend * Backend ) updateETHAccounts () error {
615+ defer backend .accountsAndKeystoreLock .RLock ()()
616+ backend .log .Debug ("Updating ETH accounts balances" )
617+
618+ accountsChainID := map [string ][]* eth.Account {}
619+ for _ , account := range backend .accounts {
620+ ethAccount , ok := account .(* eth.Account )
621+ if ok {
622+ ethCoin , ok := ethAccount .Coin ().(* eth.Coin )
623+ if ! ok {
624+ return errp .Newf ("expected ETH account to have ETH coin, got %T" , ethAccount .Coin ())
625+ }
626+ chainID := ethCoin .ChainIDstr ()
627+ accountsChainID [chainID ] = append (accountsChainID [chainID ], ethAccount )
628+ }
629+
630+ }
631+
632+ for chainID , ethAccounts := range accountsChainID {
633+ etherScanClient := etherscan .NewEtherScan (chainID , backend .etherScanHTTPClient , backend .etherScanRateLimiter )
634+ if err := eth .UpdateBalances (ethAccounts , etherScanClient ); err != nil {
635+ backend .log .WithError (err ).Errorf ("could not update ETH accounts for chain ID %s" , chainID )
636+ }
637+ }
638+
639+ return nil
640+ }
641+
558642// ManualReconnect triggers reconnecting to Electrum servers if their connection is down.
559643// Only coin connections that were previously established are reconnected.
560644// Calling this is a no-op for coins that are already connected.
@@ -592,13 +676,7 @@ func (backend *Backend) ManualReconnect(reconnectETH bool) {
592676 }
593677 if reconnectETH {
594678 backend .log .Info ("Reconnecting ETH accounts" )
595- for _ , account := range backend .accounts {
596- ethAccount , ok := account .(* eth.Account )
597- if ! ok {
598- continue
599- }
600- ethAccount .EnqueueUpdate ()
601- }
679+ backend .updateETHAccountsCh <- struct {}{}
602680 }
603681}
604682
@@ -661,6 +739,8 @@ func (backend *Backend) Start() <-chan interface{} {
661739
662740 backend .environment .OnAuthSettingChanged (backend .config .AppConfig ().Backend .Authentication )
663741
742+ go backend .pollETHAccounts ()
743+
664744 if backend .config .AppConfig ().Backend .StartInTestnet {
665745 if err := backend .config .ModifyAppConfig (func (c * config.AppConfig ) error { c .Backend .StartInTestnet = false ; return nil }); err != nil {
666746 backend .log .WithError (err ).Error ("Can't set StartInTestnet to false" )
@@ -933,6 +1013,8 @@ func (backend *Backend) Close() error {
9331013 if len (errors ) > 0 {
9341014 return errp .New (strings .Join (errors , "; " ))
9351015 }
1016+
1017+ close (backend .quit )
9361018 return nil
9371019}
9381020
0 commit comments