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 }