github.com/status-im/status-go@v1.1.0/services/wallet/reader.go (about)

     1  package wallet
     2  
     3  import (
     4  	"context"
     5  	"math"
     6  	"math/big"
     7  	"sync"
     8  	"time"
     9  
    10  	"golang.org/x/exp/maps"
    11  
    12  	"github.com/ethereum/go-ethereum/common"
    13  	"github.com/ethereum/go-ethereum/common/hexutil"
    14  	"github.com/ethereum/go-ethereum/event"
    15  	"github.com/ethereum/go-ethereum/log"
    16  	"github.com/status-im/status-go/rpc/chain"
    17  	"github.com/status-im/status-go/services/wallet/async"
    18  	"github.com/status-im/status-go/services/wallet/market"
    19  	"github.com/status-im/status-go/services/wallet/thirdparty"
    20  	"github.com/status-im/status-go/services/wallet/token"
    21  	"github.com/status-im/status-go/services/wallet/transfer"
    22  	"github.com/status-im/status-go/services/wallet/walletevent"
    23  )
    24  
    25  // WalletTickReload emitted every 15mn to reload the wallet balance and history
    26  const EventWalletTickReload walletevent.EventType = "wallet-tick-reload"
    27  const EventWalletTickCheckConnected walletevent.EventType = "wallet-tick-check-connected"
    28  
    29  const (
    30  	walletTickReloadPeriod      = 10 * time.Minute
    31  	activityReloadDelay         = 30 // Wait this many seconds after activity is detected before triggering a wallet reload
    32  	activityReloadMarginSeconds = 30 // Trigger a wallet reload if activity is detected this many seconds before the last reload
    33  )
    34  
    35  func getFixedCurrencies() []string {
    36  	return []string{"USD"}
    37  }
    38  
    39  func belongsToMandatoryTokens(symbol string) bool {
    40  	var mandatoryTokens = []string{"ETH", "DAI", "SNT", "STT"}
    41  	for _, t := range mandatoryTokens {
    42  		if t == symbol {
    43  			return true
    44  		}
    45  	}
    46  	return false
    47  }
    48  
    49  func NewReader(tokenManager token.ManagerInterface, marketManager *market.Manager, persistence token.TokenBalancesStorage, walletFeed *event.Feed) *Reader {
    50  	return &Reader{
    51  		tokenManager:        tokenManager,
    52  		marketManager:       marketManager,
    53  		persistence:         persistence,
    54  		walletFeed:          walletFeed,
    55  		refreshBalanceCache: true,
    56  	}
    57  }
    58  
    59  type Reader struct {
    60  	tokenManager                   token.ManagerInterface
    61  	marketManager                  *market.Manager
    62  	persistence                    token.TokenBalancesStorage
    63  	walletFeed                     *event.Feed
    64  	cancel                         context.CancelFunc
    65  	walletEventsWatcher            *walletevent.Watcher
    66  	lastWalletTokenUpdateTimestamp sync.Map
    67  	reloadDelayTimer               *time.Timer
    68  	refreshBalanceCache            bool
    69  	rw                             sync.RWMutex
    70  }
    71  
    72  func splitVerifiedTokens(tokens []*token.Token) ([]*token.Token, []*token.Token) {
    73  	verified := make([]*token.Token, 0)
    74  	unverified := make([]*token.Token, 0)
    75  
    76  	for _, t := range tokens {
    77  		if t.Verified {
    78  			verified = append(verified, t)
    79  		} else {
    80  			unverified = append(unverified, t)
    81  		}
    82  	}
    83  
    84  	return verified, unverified
    85  }
    86  
    87  func getTokenBySymbols(tokens []*token.Token) map[string][]*token.Token {
    88  	res := make(map[string][]*token.Token)
    89  
    90  	for _, t := range tokens {
    91  		if _, ok := res[t.Symbol]; !ok {
    92  			res[t.Symbol] = make([]*token.Token, 0)
    93  		}
    94  
    95  		res[t.Symbol] = append(res[t.Symbol], t)
    96  	}
    97  
    98  	return res
    99  }
   100  
   101  func getTokenAddresses(tokens []*token.Token) []common.Address {
   102  	set := make(map[common.Address]bool)
   103  	for _, token := range tokens {
   104  		set[token.Address] = true
   105  	}
   106  	res := make([]common.Address, 0)
   107  	for address := range set {
   108  		res = append(res, address)
   109  	}
   110  	return res
   111  }
   112  
   113  func (r *Reader) Start() error {
   114  	ctx, cancel := context.WithCancel(context.Background())
   115  	r.cancel = cancel
   116  
   117  	r.startWalletEventsWatcher()
   118  
   119  	go func() {
   120  		ticker := time.NewTicker(walletTickReloadPeriod)
   121  		defer ticker.Stop()
   122  		for {
   123  			select {
   124  			case <-ctx.Done():
   125  				return
   126  			case <-ticker.C:
   127  				r.triggerWalletReload()
   128  			}
   129  		}
   130  	}()
   131  	return nil
   132  }
   133  
   134  func (r *Reader) Stop() {
   135  	if r.cancel != nil {
   136  		r.cancel()
   137  	}
   138  
   139  	r.stopWalletEventsWatcher()
   140  
   141  	r.cancelDelayedWalletReload()
   142  
   143  	r.lastWalletTokenUpdateTimestamp = sync.Map{}
   144  }
   145  
   146  func (r *Reader) Restart() error {
   147  	r.Stop()
   148  	return r.Start()
   149  }
   150  
   151  func (r *Reader) triggerWalletReload() {
   152  	r.cancelDelayedWalletReload()
   153  
   154  	r.walletFeed.Send(walletevent.Event{
   155  		Type: EventWalletTickReload,
   156  	})
   157  }
   158  
   159  func (r *Reader) triggerDelayedWalletReload() {
   160  	r.cancelDelayedWalletReload()
   161  
   162  	r.reloadDelayTimer = time.AfterFunc(time.Duration(activityReloadDelay)*time.Second, r.triggerWalletReload)
   163  }
   164  
   165  func (r *Reader) cancelDelayedWalletReload() {
   166  
   167  	if r.reloadDelayTimer != nil {
   168  		r.reloadDelayTimer.Stop()
   169  		r.reloadDelayTimer = nil
   170  	}
   171  }
   172  
   173  func (r *Reader) startWalletEventsWatcher() {
   174  	if r.walletEventsWatcher != nil {
   175  		return
   176  	}
   177  
   178  	// Respond to ETH/Token transfers
   179  	walletEventCb := func(event walletevent.Event) {
   180  		if event.Type != transfer.EventInternalETHTransferDetected &&
   181  			event.Type != transfer.EventInternalERC20TransferDetected {
   182  			return
   183  		}
   184  
   185  		for _, address := range event.Accounts {
   186  			timestamp, ok := r.lastWalletTokenUpdateTimestamp.Load(address)
   187  			timecheck := int64(0)
   188  			if ok {
   189  				timecheck = timestamp.(int64) - activityReloadMarginSeconds
   190  			}
   191  
   192  			if !ok || event.At > timecheck {
   193  				r.triggerDelayedWalletReload()
   194  				r.invalidateBalanceCache()
   195  				break
   196  			}
   197  		}
   198  	}
   199  
   200  	r.walletEventsWatcher = walletevent.NewWatcher(r.walletFeed, walletEventCb)
   201  
   202  	r.walletEventsWatcher.Start()
   203  }
   204  
   205  func (r *Reader) stopWalletEventsWatcher() {
   206  	if r.walletEventsWatcher != nil {
   207  		r.walletEventsWatcher.Stop()
   208  		r.walletEventsWatcher = nil
   209  	}
   210  }
   211  
   212  func (r *Reader) tokensCachedForAddresses(addresses []common.Address) bool {
   213  	cachedTokens, err := r.getCachedWalletTokensWithoutMarketData()
   214  	if err != nil {
   215  		return false
   216  	}
   217  
   218  	for _, address := range addresses {
   219  		_, ok := cachedTokens[address]
   220  		if !ok {
   221  			return false
   222  		}
   223  	}
   224  
   225  	return true
   226  }
   227  
   228  func (r *Reader) isCacheTimestampValidForAddress(address common.Address) bool {
   229  	_, ok := r.lastWalletTokenUpdateTimestamp.Load(address)
   230  	return ok
   231  }
   232  
   233  func (r *Reader) areCacheTimestampsValid(addresses []common.Address) bool {
   234  	for _, address := range addresses {
   235  		if !r.isCacheTimestampValidForAddress(address) {
   236  			return false
   237  		}
   238  	}
   239  
   240  	return true
   241  }
   242  
   243  func (r *Reader) isBalanceCacheValid(addresses []common.Address) bool {
   244  	r.rw.RLock()
   245  	defer r.rw.RUnlock()
   246  
   247  	return !r.refreshBalanceCache && r.tokensCachedForAddresses(addresses) && r.areCacheTimestampsValid(addresses)
   248  }
   249  
   250  func (r *Reader) balanceRefreshed() {
   251  	r.rw.Lock()
   252  	defer r.rw.Unlock()
   253  
   254  	r.refreshBalanceCache = false
   255  }
   256  
   257  func (r *Reader) invalidateBalanceCache() {
   258  	r.rw.Lock()
   259  	defer r.rw.Unlock()
   260  
   261  	r.refreshBalanceCache = true
   262  }
   263  
   264  func (r *Reader) FetchOrGetCachedWalletBalances(ctx context.Context, clients map[uint64]chain.ClientInterface, addresses []common.Address, forceRefresh bool) (map[common.Address][]token.StorageToken, error) {
   265  	needFetch := forceRefresh || !r.isBalanceCacheValid(addresses) || r.isBalanceUpdateNeededAnyway(clients, addresses)
   266  
   267  	if needFetch {
   268  		_, err := r.FetchBalances(ctx, clients, addresses)
   269  		if err != nil {
   270  			log.Error("FetchOrGetCachedWalletBalances error", "err", err)
   271  		}
   272  	}
   273  
   274  	return r.GetCachedBalances(clients, addresses)
   275  }
   276  
   277  func (r *Reader) isBalanceUpdateNeededAnyway(clients map[uint64]chain.ClientInterface, addresses []common.Address) bool {
   278  	cachedTokens, err := r.getCachedWalletTokensWithoutMarketData()
   279  	if err != nil {
   280  		return true
   281  	}
   282  
   283  	chainIDs := maps.Keys(clients)
   284  	updateAnyway := false
   285  	for _, address := range addresses {
   286  		if res, ok := cachedTokens[address]; !ok || len(res) == 0 {
   287  			updateAnyway = true
   288  			break
   289  		}
   290  
   291  		networkFound := map[uint64]bool{}
   292  		for _, token := range cachedTokens[address] {
   293  			for _, chain := range chainIDs {
   294  				if _, ok := token.BalancesPerChain[chain]; ok {
   295  					networkFound[chain] = true
   296  				}
   297  			}
   298  		}
   299  
   300  		for _, chain := range chainIDs {
   301  			if !networkFound[chain] {
   302  				updateAnyway = true
   303  				return updateAnyway
   304  			}
   305  		}
   306  	}
   307  
   308  	return updateAnyway
   309  }
   310  
   311  func tokensToBalancesPerChain(cachedTokens map[common.Address][]token.StorageToken) map[uint64]map[common.Address]map[common.Address]*hexutil.Big {
   312  	cachedBalancesPerChain := map[uint64]map[common.Address]map[common.Address]*hexutil.Big{}
   313  	for address, tokens := range cachedTokens {
   314  		for _, token := range tokens {
   315  			for _, balance := range token.BalancesPerChain {
   316  				if _, ok := cachedBalancesPerChain[balance.ChainID]; !ok {
   317  					cachedBalancesPerChain[balance.ChainID] = map[common.Address]map[common.Address]*hexutil.Big{}
   318  				}
   319  				if _, ok := cachedBalancesPerChain[balance.ChainID][address]; !ok {
   320  					cachedBalancesPerChain[balance.ChainID][address] = map[common.Address]*hexutil.Big{}
   321  				}
   322  
   323  				bigBalance, _ := new(big.Int).SetString(balance.RawBalance, 10)
   324  				cachedBalancesPerChain[balance.ChainID][address][balance.Address] = (*hexutil.Big)(bigBalance)
   325  			}
   326  		}
   327  	}
   328  
   329  	return cachedBalancesPerChain
   330  }
   331  
   332  func (r *Reader) fetchBalances(ctx context.Context, clients map[uint64]chain.ClientInterface, addresses []common.Address, tokenAddresses []common.Address) (map[uint64]map[common.Address]map[common.Address]*hexutil.Big, error) {
   333  	latestBalances, err := r.tokenManager.GetBalancesByChain(ctx, clients, addresses, tokenAddresses)
   334  	if err != nil {
   335  		log.Error("tokenManager.GetBalancesByChain error", "err", err)
   336  		return nil, err
   337  	}
   338  
   339  	return latestBalances, nil
   340  }
   341  
   342  func toChainBalance(
   343  	balances map[uint64]map[common.Address]map[common.Address]*hexutil.Big,
   344  	tok *token.Token,
   345  	address common.Address,
   346  	decimals uint,
   347  	cachedTokens map[common.Address][]token.StorageToken,
   348  	hasError bool,
   349  	isMandatoryToken bool,
   350  ) *token.ChainBalance {
   351  	hexBalance := &big.Int{}
   352  	if balances != nil {
   353  		hexBalance = balances[tok.ChainID][address][tok.Address].ToInt()
   354  	}
   355  
   356  	balance := big.NewFloat(0.0)
   357  	if hexBalance != nil {
   358  		balance = new(big.Float).Quo(
   359  			new(big.Float).SetInt(hexBalance),
   360  			big.NewFloat(math.Pow(10, float64(decimals))),
   361  		)
   362  	}
   363  
   364  	isVisible := balance.Cmp(big.NewFloat(0.0)) > 0 || isCachedToken(cachedTokens, address, tok.Symbol, tok.ChainID)
   365  	if !isVisible && !isMandatoryToken {
   366  		return nil
   367  	}
   368  
   369  	return &token.ChainBalance{
   370  		RawBalance:     hexBalance.String(),
   371  		Balance:        balance,
   372  		Balance1DayAgo: "0",
   373  		Address:        tok.Address,
   374  		ChainID:        tok.ChainID,
   375  		HasError:       hasError,
   376  	}
   377  }
   378  
   379  func (r *Reader) getBalance1DayAgo(balance *token.ChainBalance, dayAgoTimestamp int64, symbol string, address common.Address) (*big.Int, error) {
   380  	balance1DayAgo, err := r.tokenManager.GetTokenHistoricalBalance(address, balance.ChainID, symbol, dayAgoTimestamp)
   381  	if err != nil {
   382  		log.Error("tokenManager.GetTokenHistoricalBalance error", "err", err)
   383  		return nil, err
   384  	}
   385  
   386  	return balance1DayAgo, nil
   387  }
   388  
   389  func (r *Reader) balancesToTokensByAddress(connectedPerChain map[uint64]bool, addresses []common.Address, allTokens []*token.Token, balances map[uint64]map[common.Address]map[common.Address]*hexutil.Big, cachedTokens map[common.Address][]token.StorageToken) map[common.Address][]token.StorageToken {
   390  	verifiedTokens, unverifiedTokens := splitVerifiedTokens(allTokens)
   391  
   392  	result := make(map[common.Address][]token.StorageToken)
   393  	dayAgoTimestamp := time.Now().Add(-24 * time.Hour).Unix()
   394  
   395  	for _, address := range addresses {
   396  		for _, tokenList := range [][]*token.Token{verifiedTokens, unverifiedTokens} {
   397  			for symbol, tokens := range getTokenBySymbols(tokenList) {
   398  				balancesPerChain := r.createBalancePerChainPerSymbol(address, balances, tokens, cachedTokens, connectedPerChain, dayAgoTimestamp)
   399  				if balancesPerChain == nil {
   400  					continue
   401  				}
   402  
   403  				walletToken := token.StorageToken{
   404  					Token: token.Token{
   405  						Name:          tokens[0].Name,
   406  						Symbol:        symbol,
   407  						Decimals:      tokens[0].Decimals,
   408  						PegSymbol:     token.GetTokenPegSymbol(symbol),
   409  						Verified:      tokens[0].Verified,
   410  						CommunityData: tokens[0].CommunityData,
   411  						Image:         tokens[0].Image,
   412  					},
   413  					BalancesPerChain: balancesPerChain,
   414  				}
   415  
   416  				result[address] = append(result[address], walletToken)
   417  			}
   418  		}
   419  	}
   420  
   421  	return result
   422  }
   423  
   424  func (r *Reader) createBalancePerChainPerSymbol(
   425  	address common.Address,
   426  	balances map[uint64]map[common.Address]map[common.Address]*hexutil.Big,
   427  	tokens []*token.Token,
   428  	cachedTokens map[common.Address][]token.StorageToken,
   429  	clientConnectionPerChain map[uint64]bool,
   430  	dayAgoTimestamp int64,
   431  ) map[uint64]token.ChainBalance {
   432  	var balancesPerChain map[uint64]token.ChainBalance
   433  	decimals := tokens[0].Decimals
   434  	isMandatoryToken := belongsToMandatoryTokens(tokens[0].Symbol) // we expect all tokens in the list to have the same symbol
   435  	for _, tok := range tokens {
   436  		hasError := false
   437  		if connected, ok := clientConnectionPerChain[tok.ChainID]; ok {
   438  			hasError = !connected
   439  		}
   440  
   441  		if _, ok := balances[tok.ChainID][address][tok.Address]; !ok {
   442  			hasError = true
   443  		}
   444  
   445  		// TODO: Avoid passing the entire balances map to toChainBalance. Iterate over the balances map once and pass the balance per address per token to toChainBalance
   446  		balance := toChainBalance(balances, tok, address, decimals, cachedTokens, hasError, isMandatoryToken)
   447  		if balance != nil {
   448  			balance1DayAgo, _ := r.getBalance1DayAgo(balance, dayAgoTimestamp, tok.Symbol, address) // Ignore error
   449  			if balance1DayAgo != nil {
   450  				balance.Balance1DayAgo = balance1DayAgo.String()
   451  			}
   452  
   453  			if balancesPerChain == nil {
   454  				balancesPerChain = make(map[uint64]token.ChainBalance)
   455  			}
   456  			balancesPerChain[tok.ChainID] = *balance
   457  		}
   458  	}
   459  
   460  	return balancesPerChain
   461  }
   462  
   463  func (r *Reader) GetWalletToken(ctx context.Context, clients map[uint64]chain.ClientInterface, addresses []common.Address, currency string) (map[common.Address][]token.StorageToken, error) {
   464  	currencies := make([]string, 0)
   465  	currencies = append(currencies, currency)
   466  	currencies = append(currencies, getFixedCurrencies()...)
   467  
   468  	result, err := r.FetchOrGetCachedWalletBalances(ctx, clients, addresses, true)
   469  	if err != nil {
   470  		return nil, err
   471  	}
   472  
   473  	tokenSymbols := make([]string, 0)
   474  	for _, storageTokens := range result {
   475  		for _, t := range storageTokens {
   476  			tokenSymbols = append(tokenSymbols, t.Token.Symbol)
   477  		}
   478  	}
   479  
   480  	var (
   481  		group             = async.NewAtomicGroup(ctx)
   482  		prices            = map[string]map[string]float64{}
   483  		tokenDetails      = map[string]thirdparty.TokenDetails{}
   484  		tokenMarketValues = map[string]thirdparty.TokenMarketValues{}
   485  	)
   486  
   487  	group.Add(func(parent context.Context) error {
   488  		prices, err = r.marketManager.FetchPrices(tokenSymbols, currencies)
   489  		if err != nil {
   490  			log.Info("marketManager.FetchPrices err", err)
   491  		}
   492  		return nil
   493  	})
   494  
   495  	group.Add(func(parent context.Context) error {
   496  		tokenDetails, err = r.marketManager.FetchTokenDetails(tokenSymbols)
   497  		if err != nil {
   498  			log.Info("marketManager.FetchTokenDetails err", err)
   499  		}
   500  		return nil
   501  	})
   502  
   503  	group.Add(func(parent context.Context) error {
   504  		tokenMarketValues, err = r.marketManager.FetchTokenMarketValues(tokenSymbols, currency)
   505  		if err != nil {
   506  			log.Info("marketManager.FetchTokenMarketValues err", err)
   507  		}
   508  		return nil
   509  	})
   510  
   511  	select {
   512  	case <-group.WaitAsync():
   513  	case <-ctx.Done():
   514  		return nil, ctx.Err()
   515  	}
   516  	err = group.Error()
   517  	if err != nil {
   518  		return nil, err
   519  	}
   520  
   521  	for address, tokens := range result {
   522  		for index, tok := range tokens {
   523  			marketValuesPerCurrency := make(map[string]token.TokenMarketValues)
   524  			for _, currency := range currencies {
   525  				if _, ok := tokenMarketValues[tok.Symbol]; !ok {
   526  					continue
   527  				}
   528  				marketValuesPerCurrency[currency] = token.TokenMarketValues{
   529  					MarketCap:       tokenMarketValues[tok.Symbol].MKTCAP,
   530  					HighDay:         tokenMarketValues[tok.Symbol].HIGHDAY,
   531  					LowDay:          tokenMarketValues[tok.Symbol].LOWDAY,
   532  					ChangePctHour:   tokenMarketValues[tok.Symbol].CHANGEPCTHOUR,
   533  					ChangePctDay:    tokenMarketValues[tok.Symbol].CHANGEPCTDAY,
   534  					ChangePct24hour: tokenMarketValues[tok.Symbol].CHANGEPCT24HOUR,
   535  					Change24hour:    tokenMarketValues[tok.Symbol].CHANGE24HOUR,
   536  					Price:           prices[tok.Symbol][currency],
   537  					HasError:        !r.marketManager.IsConnected,
   538  				}
   539  			}
   540  
   541  			if _, ok := tokenDetails[tok.Symbol]; !ok {
   542  				continue
   543  			}
   544  
   545  			result[address][index].Description = tokenDetails[tok.Symbol].Description
   546  			result[address][index].AssetWebsiteURL = tokenDetails[tok.Symbol].AssetWebsiteURL
   547  			result[address][index].BuiltOn = tokenDetails[tok.Symbol].BuiltOn
   548  			result[address][index].MarketValuesPerCurrency = marketValuesPerCurrency
   549  		}
   550  	}
   551  
   552  	r.updateTokenUpdateTimestamp(addresses)
   553  
   554  	return result, r.persistence.SaveTokens(result)
   555  }
   556  
   557  func isCachedToken(cachedTokens map[common.Address][]token.StorageToken, address common.Address, symbol string, chainID uint64) bool {
   558  	if tokens, ok := cachedTokens[address]; ok {
   559  		for _, t := range tokens {
   560  			if t.Symbol != symbol {
   561  				continue
   562  			}
   563  			_, ok := t.BalancesPerChain[chainID]
   564  			if ok {
   565  				return true
   566  			}
   567  		}
   568  	}
   569  	return false
   570  }
   571  
   572  // getCachedWalletTokensWithoutMarketData returns the latest fetched balances, minus
   573  // price information
   574  func (r *Reader) getCachedWalletTokensWithoutMarketData() (map[common.Address][]token.StorageToken, error) {
   575  	return r.persistence.GetTokens()
   576  }
   577  
   578  func (r *Reader) updateTokenUpdateTimestamp(addresses []common.Address) {
   579  	for _, address := range addresses {
   580  		r.lastWalletTokenUpdateTimestamp.Store(address, time.Now().Unix())
   581  	}
   582  }
   583  
   584  func (r *Reader) FetchBalances(ctx context.Context, clients map[uint64]chain.ClientInterface, addresses []common.Address) (map[common.Address][]token.StorageToken, error) {
   585  	cachedTokens, err := r.getCachedWalletTokensWithoutMarketData()
   586  	if err != nil {
   587  		return nil, err
   588  	}
   589  
   590  	chainIDs := maps.Keys(clients)
   591  	allTokens, err := r.tokenManager.GetTokensByChainIDs(chainIDs)
   592  	if err != nil {
   593  		return nil, err
   594  	}
   595  
   596  	tokenAddresses := getTokenAddresses(allTokens)
   597  	balances, err := r.fetchBalances(ctx, clients, addresses, tokenAddresses)
   598  	if err != nil {
   599  		log.Error("failed to update balances", "err", err)
   600  		return nil, err
   601  	}
   602  
   603  	connectedPerChain := map[uint64]bool{}
   604  	for chainID, client := range clients {
   605  		connectedPerChain[chainID] = client.IsConnected()
   606  	}
   607  
   608  	tokens := r.balancesToTokensByAddress(connectedPerChain, addresses, allTokens, balances, cachedTokens)
   609  
   610  	err = r.persistence.SaveTokens(tokens)
   611  	if err != nil {
   612  		log.Error("failed to save tokens", "err", err) // Do not return error, as it is not critical
   613  	}
   614  
   615  	r.updateTokenUpdateTimestamp(addresses)
   616  	r.balanceRefreshed()
   617  
   618  	return tokens, err
   619  }
   620  
   621  func (r *Reader) GetCachedBalances(clients map[uint64]chain.ClientInterface, addresses []common.Address) (map[common.Address][]token.StorageToken, error) {
   622  	cachedTokens, err := r.getCachedWalletTokensWithoutMarketData()
   623  	if err != nil {
   624  		return nil, err
   625  	}
   626  
   627  	chainIDs := maps.Keys(clients)
   628  	allTokens, err := r.tokenManager.GetTokensByChainIDs(chainIDs)
   629  	if err != nil {
   630  		return nil, err
   631  	}
   632  
   633  	connectedPerChain := map[uint64]bool{}
   634  	for chainID, client := range clients {
   635  		connectedPerChain[chainID] = client.IsConnected()
   636  	}
   637  
   638  	balances := tokensToBalancesPerChain(cachedTokens)
   639  	return r.balancesToTokensByAddress(connectedPerChain, addresses, allTokens, balances, cachedTokens), nil
   640  }