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

     1  package history
     2  
     3  import (
     4  	"context"
     5  	"database/sql"
     6  	"errors"
     7  	"fmt"
     8  	"math"
     9  	"math/big"
    10  	"reflect"
    11  	"sort"
    12  	"time"
    13  
    14  	"github.com/ethereum/go-ethereum/common"
    15  	"github.com/ethereum/go-ethereum/common/hexutil"
    16  	"github.com/ethereum/go-ethereum/event"
    17  	"github.com/ethereum/go-ethereum/log"
    18  
    19  	statustypes "github.com/status-im/status-go/eth-node/types"
    20  	"github.com/status-im/status-go/multiaccounts/accounts"
    21  	"github.com/status-im/status-go/params"
    22  	statusrpc "github.com/status-im/status-go/rpc"
    23  	"github.com/status-im/status-go/rpc/chain"
    24  	"github.com/status-im/status-go/rpc/network"
    25  
    26  	"github.com/status-im/status-go/services/accounts/accountsevent"
    27  	"github.com/status-im/status-go/services/wallet/balance"
    28  	"github.com/status-im/status-go/services/wallet/market"
    29  	"github.com/status-im/status-go/services/wallet/token"
    30  	"github.com/status-im/status-go/services/wallet/transfer"
    31  	"github.com/status-im/status-go/services/wallet/walletevent"
    32  )
    33  
    34  const minPointsForGraph = 14 // for minimal time frame - 7 days, twice a day
    35  
    36  // EventBalanceHistoryUpdateStarted and EventBalanceHistoryUpdateDone are used to notify the UI that balance history is being updated
    37  const (
    38  	EventBalanceHistoryUpdateStarted           walletevent.EventType = "wallet-balance-history-update-started"
    39  	EventBalanceHistoryUpdateFinished          walletevent.EventType = "wallet-balance-history-update-finished"
    40  	EventBalanceHistoryUpdateFinishedWithError walletevent.EventType = "wallet-balance-history-update-finished-with-error"
    41  )
    42  
    43  type ValuePoint struct {
    44  	Value     float64 `json:"value"`
    45  	Timestamp uint64  `json:"time"`
    46  }
    47  
    48  func (vp *ValuePoint) String() string {
    49  	return fmt.Sprintf("%d: %f", vp.Timestamp, vp.Value)
    50  }
    51  
    52  type Service struct {
    53  	balance         *Balance
    54  	db              *sql.DB
    55  	accountsDB      *accounts.Database
    56  	accountFeed     *event.Feed
    57  	eventFeed       *event.Feed
    58  	rpcClient       *statusrpc.Client
    59  	networkManager  *network.Manager
    60  	tokenManager    *token.Manager
    61  	serviceContext  context.Context
    62  	cancelFn        context.CancelFunc
    63  	transferWatcher *Watcher
    64  	accWatcher      *accountsevent.Watcher
    65  	exchange        *Exchange
    66  	balanceCache    balance.CacheIface
    67  }
    68  
    69  func NewService(db *sql.DB, accountsDB *accounts.Database, accountFeed *event.Feed, eventFeed *event.Feed, rpcClient *statusrpc.Client, tokenManager *token.Manager, marketManager *market.Manager, balanceCache balance.CacheIface) *Service {
    70  	return &Service{
    71  		balance:        NewBalance(NewBalanceDB(db)),
    72  		db:             db,
    73  		accountsDB:     accountsDB,
    74  		accountFeed:    accountFeed,
    75  		eventFeed:      eventFeed,
    76  		rpcClient:      rpcClient,
    77  		networkManager: rpcClient.NetworkManager,
    78  		tokenManager:   tokenManager,
    79  		exchange:       NewExchange(marketManager),
    80  		balanceCache:   balanceCache,
    81  	}
    82  }
    83  
    84  func (s *Service) Stop() {
    85  	if s.cancelFn != nil {
    86  		s.cancelFn()
    87  	}
    88  
    89  	s.stopTransfersWatcher()
    90  	s.stopAccountWatcher()
    91  }
    92  
    93  func (s *Service) triggerEvent(eventType walletevent.EventType, account statustypes.Address, message string) {
    94  	s.eventFeed.Send(walletevent.Event{
    95  		Type: eventType,
    96  		Accounts: []common.Address{
    97  			common.Address(account),
    98  		},
    99  		Message: message,
   100  	})
   101  }
   102  
   103  func (s *Service) Start() {
   104  	log.Debug("Starting balance history service")
   105  
   106  	s.startTransfersWatcher()
   107  	s.startAccountWatcher()
   108  
   109  	go func() {
   110  		s.serviceContext, s.cancelFn = context.WithCancel(context.Background())
   111  
   112  		err := s.updateBalanceHistory(s.serviceContext)
   113  		if s.serviceContext.Err() != nil {
   114  			s.triggerEvent(EventBalanceHistoryUpdateFinished, statustypes.Address{}, "Service canceled")
   115  		}
   116  		if err != nil {
   117  			s.triggerEvent(EventBalanceHistoryUpdateFinishedWithError, statustypes.Address{}, err.Error())
   118  		}
   119  	}()
   120  }
   121  
   122  func (s *Service) mergeChainsBalances(chainIDs []uint64, addresses []common.Address, tokenSymbol string, fromTimestamp uint64, data map[uint64][]*entry) ([]*DataPoint, error) {
   123  	log.Debug("Merging balances", "address", addresses, "tokenSymbol", tokenSymbol, "fromTimestamp", fromTimestamp, "len(data)", len(data))
   124  
   125  	toTimestamp := uint64(time.Now().UTC().Unix())
   126  	allData := make([]*entry, 0)
   127  
   128  	// Add edge points per chain
   129  	// Iterate over chainIDs param, not data keys, because data may not contain all the chains, but we need edge points for all of them
   130  	for _, chainID := range chainIDs {
   131  		// edge points are needed to properly calculate total balance, as they contain the balance for the first and last timestamp
   132  		chainData, err := s.balance.addEdgePoints(chainID, tokenSymbol, addresses, fromTimestamp, toTimestamp, data[chainID])
   133  		if err != nil {
   134  			return nil, err
   135  		}
   136  		allData = append(allData, chainData...)
   137  	}
   138  
   139  	// Sort by timestamp
   140  	sort.Slice(allData, func(i, j int) bool {
   141  		return allData[i].timestamp < allData[j].timestamp
   142  	})
   143  
   144  	// Add padding points to make chart look nice
   145  	numEdgePoints := 2 * len(chainIDs) // 2 edge points per chain
   146  	if len(allData) < minPointsForGraph {
   147  		allData, _ = addPaddingPoints(tokenSymbol, addresses, toTimestamp, allData, minPointsForGraph+numEdgePoints)
   148  	}
   149  
   150  	return entriesToDataPoints(allData)
   151  }
   152  
   153  // Expects sorted data
   154  func entriesToDataPoints(data []*entry) ([]*DataPoint, error) {
   155  	var resSlice []*DataPoint
   156  	var groupedEntries []*entry // Entries with the same timestamp
   157  
   158  	type AddressKey struct {
   159  		Address common.Address
   160  		ChainID uint64
   161  	}
   162  
   163  	sumBalances := func(balanceMap map[AddressKey]*big.Int) *big.Int {
   164  		// Sum balances of all accounts and chains in current timestamp
   165  		sum := big.NewInt(0)
   166  		for _, balance := range balanceMap {
   167  			sum.Add(sum, balance)
   168  		}
   169  		return sum
   170  	}
   171  
   172  	updateBalanceMap := func(balanceMap map[AddressKey]*big.Int, entries []*entry) map[AddressKey]*big.Int {
   173  		// Update balance map for this timestamp
   174  		for _, entry := range entries {
   175  			key := AddressKey{
   176  				Address: entry.address,
   177  				ChainID: entry.chainID,
   178  			}
   179  			balanceMap[key] = entry.balance
   180  		}
   181  		return balanceMap
   182  	}
   183  
   184  	// Balance map always contains current balance for each address in specific timestamp
   185  	// It is required to sum up balances from previous timestamp from accounts not present in current timestamp
   186  	balanceMap := make(map[AddressKey]*big.Int)
   187  
   188  	for _, entry := range data {
   189  		if len(groupedEntries) > 0 {
   190  			if entry.timestamp == groupedEntries[0].timestamp {
   191  				groupedEntries = append(groupedEntries, entry)
   192  				continue
   193  			} else {
   194  				// Split grouped entries into addresses
   195  				balanceMap = updateBalanceMap(balanceMap, groupedEntries)
   196  				// Calculate balance for all the addresses
   197  				cumulativeBalance := sumBalances(balanceMap)
   198  				// Points in slice contain balances for all chains
   199  				resSlice = appendPointToSlice(resSlice, &DataPoint{
   200  					Timestamp: uint64(groupedEntries[0].timestamp),
   201  					Balance:   (*hexutil.Big)(cumulativeBalance),
   202  				})
   203  
   204  				// Reset grouped entries
   205  				groupedEntries = nil
   206  				groupedEntries = append(groupedEntries, entry)
   207  			}
   208  		} else {
   209  			groupedEntries = append(groupedEntries, entry)
   210  		}
   211  	}
   212  
   213  	// If only edge points are present, groupedEntries will be non-empty
   214  	if len(groupedEntries) > 0 {
   215  		// Split grouped entries into addresses
   216  		balanceMap = updateBalanceMap(balanceMap, groupedEntries)
   217  		// Calculate balance for all the addresses
   218  		cumulativeBalance := sumBalances(balanceMap)
   219  		resSlice = appendPointToSlice(resSlice, &DataPoint{
   220  			Timestamp: uint64(groupedEntries[0].timestamp),
   221  			Balance:   (*hexutil.Big)(cumulativeBalance),
   222  		})
   223  	}
   224  
   225  	return resSlice, nil
   226  }
   227  
   228  func appendPointToSlice(slice []*DataPoint, point *DataPoint) []*DataPoint {
   229  	// Replace the last point in slice if it has the same timestamp or add a new one if different
   230  	if len(slice) > 0 {
   231  		if slice[len(slice)-1].Timestamp != point.Timestamp {
   232  			// Timestamps are different, appending to slice
   233  			slice = append(slice, point)
   234  		} else {
   235  			// Replace last item in slice because timestamps are the same
   236  			slice[len(slice)-1] = point
   237  		}
   238  	} else {
   239  		slice = append(slice, point)
   240  	}
   241  
   242  	return slice
   243  }
   244  
   245  // GetBalanceHistory returns token count balance
   246  func (s *Service) GetBalanceHistory(ctx context.Context, chainIDs []uint64, addresses []common.Address, tokenSymbol string, currencySymbol string, fromTimestamp uint64) ([]*ValuePoint, error) {
   247  	log.Debug("GetBalanceHistory", "chainIDs", chainIDs, "address", addresses, "tokenSymbol", tokenSymbol, "currencySymbol", currencySymbol, "fromTimestamp", fromTimestamp)
   248  
   249  	chainDataMap := make(map[uint64][]*entry)
   250  	for _, chainID := range chainIDs {
   251  		chainData, err := s.balance.get(ctx, chainID, tokenSymbol, addresses, fromTimestamp) // TODO Make chainID a slice?
   252  		if err != nil {
   253  			return nil, err
   254  		}
   255  
   256  		if len(chainData) == 0 {
   257  			continue
   258  		}
   259  
   260  		chainDataMap[chainID] = chainData
   261  	}
   262  
   263  	// Need to get balance for all the chains for the first timestamp, otherwise total values will be incorrect
   264  	data, err := s.mergeChainsBalances(chainIDs, addresses, tokenSymbol, fromTimestamp, chainDataMap)
   265  
   266  	if err != nil {
   267  		return nil, err
   268  	} else if len(data) == 0 {
   269  		return make([]*ValuePoint, 0), nil
   270  	}
   271  
   272  	return s.dataPointsToValuePoints(chainIDs, tokenSymbol, currencySymbol, data)
   273  }
   274  
   275  func (s *Service) dataPointsToValuePoints(chainIDs []uint64, tokenSymbol string, currencySymbol string, data []*DataPoint) ([]*ValuePoint, error) {
   276  	if len(data) == 0 {
   277  		return make([]*ValuePoint, 0), nil
   278  	}
   279  
   280  	// Check if historical exchange rate for data point is present and fetch remaining if not
   281  	lastDayTime := time.Unix(int64(data[len(data)-1].Timestamp), 0).UTC()
   282  	currentTime := time.Now().UTC()
   283  	currentDayStart := time.Date(currentTime.Year(), currentTime.Month(), currentTime.Day(), 0, 0, 0, 0, time.UTC)
   284  	if lastDayTime.After(currentDayStart) {
   285  		// No chance to have today, use the previous day value for the last data point
   286  		lastDayTime = lastDayTime.AddDate(0, 0, -1)
   287  	}
   288  
   289  	lastDayValue, err := s.exchange.GetExchangeRateForDay(tokenSymbol, currencySymbol, lastDayTime)
   290  	if err != nil {
   291  		err := s.exchange.FetchAndCacheMissingRates(tokenSymbol, currencySymbol)
   292  		if err != nil {
   293  			log.Error("Error fetching exchange rates", "tokenSymbol", tokenSymbol, "currencySymbol", currencySymbol, "err", err)
   294  			return nil, err
   295  		}
   296  
   297  		lastDayValue, err = s.exchange.GetExchangeRateForDay(tokenSymbol, currencySymbol, lastDayTime)
   298  		if err != nil {
   299  			log.Error("Exchange rate missing for", "tokenSymbol", tokenSymbol, "currencySymbol", currencySymbol, "lastDayTime", lastDayTime, "err", err)
   300  			return nil, err
   301  		}
   302  	}
   303  
   304  	decimals, err := s.decimalsForToken(tokenSymbol, chainIDs[0])
   305  	if err != nil {
   306  		return nil, err
   307  	}
   308  	weisInOneMain := big.NewFloat(math.Pow(10, float64(decimals)))
   309  
   310  	var res []*ValuePoint
   311  	for _, d := range data {
   312  		var dayValue float32
   313  		dayTime := time.Unix(int64(d.Timestamp), 0).UTC()
   314  		if dayTime.After(currentDayStart) {
   315  			// No chance to have today, use the previous day value for the last data point
   316  			if lastDayValue > 0 {
   317  				dayValue = lastDayValue
   318  			} else {
   319  				log.Warn("Exchange rate missing for", "dayTime", dayTime, "err", err)
   320  				continue
   321  			}
   322  		} else {
   323  			dayValue, err = s.exchange.GetExchangeRateForDay(tokenSymbol, currencySymbol, dayTime)
   324  			if err != nil {
   325  				log.Warn("Exchange rate missing for", "dayTime", dayTime, "err", err)
   326  				continue
   327  			}
   328  		}
   329  
   330  		// The big.Int values are discarded, hence copy the original values
   331  		res = append(res, &ValuePoint{
   332  			Timestamp: d.Timestamp,
   333  			Value:     tokenToValue((*big.Int)(d.Balance), dayValue, weisInOneMain),
   334  		})
   335  	}
   336  
   337  	return res, nil
   338  }
   339  
   340  func (s *Service) decimalsForToken(tokenSymbol string, chainID uint64) (int, error) {
   341  	network := s.networkManager.Find(chainID)
   342  	if network == nil {
   343  		return 0, errors.New("network not found")
   344  	}
   345  	token := s.tokenManager.FindToken(network, tokenSymbol)
   346  	if token == nil {
   347  		return 0, errors.New("token not found")
   348  	}
   349  	return int(token.Decimals), nil
   350  }
   351  
   352  func tokenToValue(tokenCount *big.Int, mainDenominationValue float32, weisInOneMain *big.Float) float64 {
   353  	weis := new(big.Float).SetInt(tokenCount)
   354  	mainTokens := new(big.Float).Quo(weis, weisInOneMain)
   355  	mainTokenValue := new(big.Float).SetFloat64(float64(mainDenominationValue))
   356  	res, accuracy := new(big.Float).Mul(mainTokens, mainTokenValue).Float64()
   357  	if res == 0 && accuracy == big.Below {
   358  		return math.SmallestNonzeroFloat64
   359  	} else if res == math.Inf(1) && accuracy == big.Above {
   360  		return math.Inf(1)
   361  	}
   362  
   363  	return res
   364  }
   365  
   366  // updateBalanceHistory iterates over all networks depending on test/prod for the s.visibleTokenSymbol
   367  // and updates the balance history for the given address
   368  //
   369  // expects ctx to have cancellation support and processing to be cancelled by the caller
   370  func (s *Service) updateBalanceHistory(ctx context.Context) error {
   371  	log.Debug("updateBalanceHistory started")
   372  
   373  	addresses, err := s.accountsDB.GetWalletAddresses()
   374  	if err != nil {
   375  		return err
   376  	}
   377  
   378  	areTestNetworksEnabled, err := s.accountsDB.GetTestNetworksEnabled()
   379  	if err != nil {
   380  		return err
   381  	}
   382  
   383  	onlyEnabledNetworks := false
   384  	networks, err := s.networkManager.Get(onlyEnabledNetworks)
   385  	if err != nil {
   386  		return err
   387  	}
   388  
   389  	for _, address := range addresses {
   390  		s.triggerEvent(EventBalanceHistoryUpdateStarted, address, "")
   391  
   392  		for _, network := range networks {
   393  			if network.IsTest != areTestNetworksEnabled {
   394  				continue
   395  			}
   396  
   397  			entries, err := s.balance.db.getEntriesWithoutBalances(network.ChainID, common.Address(address))
   398  			if err != nil {
   399  				log.Error("Error getting blocks without balances", "chainID", network.ChainID, "address", address.String(), "err", err)
   400  				return err
   401  			}
   402  
   403  			log.Debug("Blocks without balances", "chainID", network.ChainID, "address", address.String(), "entries", entries)
   404  
   405  			client, err := s.rpcClient.EthClient(network.ChainID)
   406  			if err != nil {
   407  				log.Error("Error getting client", "chainID", network.ChainID, "address", address.String(), "err", err)
   408  				return err
   409  			}
   410  
   411  			err = s.addEntriesToDB(ctx, client, network, address, entries)
   412  			if err != nil {
   413  				return err
   414  			}
   415  		}
   416  		s.triggerEvent(EventBalanceHistoryUpdateFinished, address, "")
   417  	}
   418  
   419  	log.Debug("updateBalanceHistory finished")
   420  	return nil
   421  }
   422  
   423  func (s *Service) addEntriesToDB(ctx context.Context, client chain.ClientInterface, network *params.Network, address statustypes.Address, entries []*entry) (err error) {
   424  	for _, entry := range entries {
   425  		var balance *big.Int
   426  		// tokenAddess is zero for native currency
   427  		if (entry.tokenAddress == common.Address{}) {
   428  			// Check in cache
   429  			balance = s.balanceCache.GetBalance(common.Address(address), network.ChainID, entry.block)
   430  			log.Debug("Balance from cache", "chainID", network.ChainID, "address", address.String(), "block", entry.block, "balance", balance)
   431  
   432  			if balance == nil {
   433  				balance, err = client.BalanceAt(ctx, common.Address(address), entry.block)
   434  				if err != nil {
   435  					log.Error("Error getting balance", "chainID", network.ChainID, "address", address.String(), "err", err, "unwrapped", errors.Unwrap(err))
   436  					return err
   437  				}
   438  				time.Sleep(50 * time.Millisecond) // TODO Remove this sleep after fixing exceeding rate limit
   439  			}
   440  			entry.tokenSymbol = network.NativeCurrencySymbol
   441  		} else {
   442  			// Check token first if it is supported
   443  			token := s.tokenManager.FindTokenByAddress(network.ChainID, entry.tokenAddress)
   444  			if token == nil {
   445  				log.Warn("Token not found", "chainID", network.ChainID, "address", address.String(), "tokenAddress", entry.tokenAddress.String())
   446  				// TODO Add "supported=false" flag to such tokens to avoid checking them again and again
   447  				continue // Skip token that we don't have symbol for. For example we don't have tokens in store for goerli optimism
   448  			} else {
   449  				entry.tokenSymbol = token.Symbol
   450  			}
   451  
   452  			// Check balance for token
   453  			balance, err = s.tokenManager.GetTokenBalanceAt(ctx, client, common.Address(address), entry.tokenAddress, entry.block)
   454  			log.Debug("Balance from token manager", "chainID", network.ChainID, "address", address.String(), "block", entry.block, "balance", balance)
   455  
   456  			if err != nil {
   457  				log.Error("Error getting token balance", "chainID", network.ChainID, "address", address.String(), "tokenAddress", entry.tokenAddress.String(), "err", err)
   458  				return err
   459  			}
   460  		}
   461  
   462  		entry.balance = balance
   463  		err = s.balance.db.add(entry)
   464  		if err != nil {
   465  			log.Error("Error adding balance", "chainID", network.ChainID, "address", address.String(), "err", err)
   466  			return err
   467  		}
   468  	}
   469  
   470  	return nil
   471  }
   472  
   473  func (s *Service) startTransfersWatcher() {
   474  	if s.transferWatcher != nil {
   475  		return
   476  	}
   477  
   478  	transferLoadedCb := func(chainID uint64, addresses []common.Address, block *big.Int) {
   479  		log.Debug("Balance history watcher: transfer loaded:", "chainID", chainID, "addresses", addresses, "block", block.Uint64())
   480  
   481  		client, err := s.rpcClient.EthClient(chainID)
   482  		if err != nil {
   483  			log.Error("Error getting client", "chainID", chainID, "err", err)
   484  			return
   485  		}
   486  
   487  		network := s.networkManager.Find(chainID)
   488  		if network == nil {
   489  			log.Error("Network not found", "chainID", chainID)
   490  			return
   491  		}
   492  
   493  		transferDB := transfer.NewDB(s.db)
   494  
   495  		for _, address := range addresses {
   496  			transfers, err := transferDB.GetTransfersByAddressAndBlock(chainID, address, block, 1500) // 1500 is quite arbitrary and far from real, but should be enough to cover all transfers in a block
   497  			if err != nil {
   498  				log.Error("Error getting transfers", "chainID", chainID, "address", address.String(), "err", err)
   499  				continue
   500  			}
   501  
   502  			if len(transfers) == 0 {
   503  				log.Debug("No transfers found", "chainID", chainID, "address", address.String(), "block", block.Uint64())
   504  				continue
   505  			}
   506  
   507  			entries := transfersToEntries(address, block, transfers) // TODO Remove address and block after testing that they match
   508  			unique := removeDuplicates(entries)
   509  			log.Debug("Entries after filtering", "entries", entries, "unique", unique)
   510  
   511  			err = s.addEntriesToDB(s.serviceContext, client, network, statustypes.Address(address), unique)
   512  			if err != nil {
   513  				log.Error("Error adding entries to DB", "chainID", chainID, "address", address.String(), "err", err)
   514  				continue
   515  			}
   516  
   517  			// No event triggering here, because noone cares about balance history updates yet
   518  		}
   519  	}
   520  
   521  	s.transferWatcher = NewWatcher(s.eventFeed, transferLoadedCb)
   522  	s.transferWatcher.Start()
   523  }
   524  
   525  func removeDuplicates(entries []*entry) []*entry {
   526  	unique := make([]*entry, 0, len(entries))
   527  	for _, entry := range entries {
   528  		found := false
   529  		for _, u := range unique {
   530  			if reflect.DeepEqual(entry, u) {
   531  				found = true
   532  				break
   533  			}
   534  		}
   535  		if !found {
   536  			unique = append(unique, entry)
   537  		}
   538  	}
   539  
   540  	return unique
   541  }
   542  
   543  func transfersToEntries(address common.Address, block *big.Int, transfers []transfer.Transfer) []*entry {
   544  	entries := make([]*entry, 0)
   545  
   546  	for _, transfer := range transfers {
   547  		entry := &entry{
   548  			chainID:      transfer.NetworkID,
   549  			address:      transfer.Address,
   550  			tokenAddress: transfer.Log.Address,
   551  			block:        transfer.BlockNumber,
   552  			timestamp:    (int64)(transfer.Timestamp),
   553  		}
   554  
   555  		entries = append(entries, entry)
   556  	}
   557  
   558  	return entries
   559  }
   560  
   561  func (s *Service) stopTransfersWatcher() {
   562  	if s.transferWatcher != nil {
   563  		s.transferWatcher.Stop()
   564  		s.transferWatcher = nil
   565  	}
   566  }
   567  
   568  func (s *Service) startAccountWatcher() {
   569  	if s.accWatcher == nil {
   570  		s.accWatcher = accountsevent.NewWatcher(s.accountsDB, s.accountFeed, func(changedAddresses []common.Address, eventType accountsevent.EventType, currentAddresses []common.Address) {
   571  			s.onAccountsChanged(changedAddresses, eventType, currentAddresses)
   572  		})
   573  	}
   574  	s.accWatcher.Start()
   575  }
   576  
   577  func (s *Service) stopAccountWatcher() {
   578  	if s.accWatcher != nil {
   579  		s.accWatcher.Stop()
   580  		s.accWatcher = nil
   581  	}
   582  }
   583  
   584  func (s *Service) onAccountsChanged(changedAddresses []common.Address, eventType accountsevent.EventType, currentAddresses []common.Address) {
   585  	if eventType == accountsevent.EventTypeRemoved {
   586  		for _, address := range changedAddresses {
   587  			err := s.balance.db.removeBalanceHistory(address)
   588  			if err != nil {
   589  				log.Error("Error removing balance history", "address", address, "err", err)
   590  			}
   591  		}
   592  	}
   593  }