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 }