github.com/status-im/status-go@v1.1.0/services/wallet/token/token.go (about) 1 package token 2 3 import ( 4 "context" 5 "database/sql" 6 "encoding/json" 7 "errors" 8 "fmt" 9 "math/big" 10 "strconv" 11 "strings" 12 "sync" 13 "time" 14 15 "github.com/ethereum/go-ethereum/accounts/abi/bind" 16 "github.com/ethereum/go-ethereum/common" 17 "github.com/ethereum/go-ethereum/common/hexutil" 18 "github.com/ethereum/go-ethereum/event" 19 "github.com/ethereum/go-ethereum/log" 20 "github.com/status-im/status-go/contracts" 21 "github.com/status-im/status-go/contracts/community-tokens/assets" 22 eth_node_types "github.com/status-im/status-go/eth-node/types" 23 "github.com/status-im/status-go/multiaccounts/accounts" 24 "github.com/status-im/status-go/params" 25 "github.com/status-im/status-go/protocol/communities/token" 26 "github.com/status-im/status-go/rpc" 27 "github.com/status-im/status-go/rpc/network" 28 "github.com/status-im/status-go/server" 29 "github.com/status-im/status-go/services/accounts/accountsevent" 30 "github.com/status-im/status-go/services/communitytokens/communitytokensdatabase" 31 "github.com/status-im/status-go/services/utils" 32 "github.com/status-im/status-go/services/wallet/bigint" 33 "github.com/status-im/status-go/services/wallet/community" 34 "github.com/status-im/status-go/services/wallet/token/balancefetcher" 35 "github.com/status-im/status-go/services/wallet/walletevent" 36 ) 37 38 const ( 39 EventCommunityTokenReceived walletevent.EventType = "wallet-community-token-received" 40 ) 41 42 type Token struct { 43 Address common.Address `json:"address"` 44 Name string `json:"name"` 45 Symbol string `json:"symbol"` 46 // Decimals defines how divisible the token is. For example, 0 would be 47 // indivisible, whereas 18 would allow very small amounts of the token 48 // to be traded. 49 Decimals uint `json:"decimals"` 50 ChainID uint64 `json:"chainId"` 51 // PegSymbol indicates that the token is pegged to some fiat currency, using the 52 // ISO 4217 alphabetic code. For example, an empty string means it is not 53 // pegged, while "USD" means it's pegged to the United States Dollar. 54 PegSymbol string `json:"pegSymbol"` 55 Image string `json:"image,omitempty"` 56 57 CommunityData *community.Data `json:"community_data,omitempty"` 58 Verified bool `json:"verified"` 59 TokenListID string `json:"tokenListId"` 60 } 61 62 type ReceivedToken struct { 63 Token 64 Amount float64 `json:"amount"` 65 TxHash common.Hash `json:"txHash"` 66 IsFirst bool `json:"isFirst"` 67 } 68 69 func (t *Token) IsNative() bool { 70 return strings.EqualFold(t.Symbol, "ETH") 71 } 72 73 type List struct { 74 Name string `json:"name"` 75 Tokens []*Token `json:"tokens"` 76 Source string `json:"source"` 77 Version string `json:"version"` 78 } 79 80 type ListWrapper struct { 81 UpdatedAt int64 `json:"updatedAt"` 82 Data []*List `json:"data"` 83 } 84 85 type addressTokenMap = map[common.Address]*Token 86 type storeMap = map[uint64]addressTokenMap 87 88 type ManagerInterface interface { 89 balancefetcher.BalanceFetcher 90 LookupTokenIdentity(chainID uint64, address common.Address, native bool) *Token 91 LookupToken(chainID *uint64, tokenSymbol string) (token *Token, isNative bool) 92 GetTokenHistoricalBalance(account common.Address, chainID uint64, symbol string, timestamp int64) (*big.Int, error) 93 GetTokensByChainIDs(chainIDs []uint64) ([]*Token, error) 94 } 95 96 // Manager is used for accessing token store. It changes the token store based on overridden tokens 97 type Manager struct { 98 balancefetcher.BalanceFetcher 99 db *sql.DB 100 RPCClient rpc.ClientInterface 101 ContractMaker *contracts.ContractMaker 102 networkManager network.ManagerInterface 103 stores []store // Set on init, not changed afterwards 104 communityTokensDB *communitytokensdatabase.Database 105 communityManager *community.Manager 106 mediaServer *server.MediaServer 107 walletFeed *event.Feed 108 accountFeed *event.Feed 109 accountWatcher *accountsevent.Watcher 110 accountsDB *accounts.Database 111 tokenBalancesStorage TokenBalancesStorage 112 113 tokens []*Token 114 115 tokenLock sync.RWMutex 116 } 117 118 func mergeTokens(sliceLists [][]*Token) []*Token { 119 allKeys := make(map[string]bool) 120 res := []*Token{} 121 for _, list := range sliceLists { 122 for _, token := range list { 123 key := strconv.FormatUint(token.ChainID, 10) + token.Address.String() 124 if _, value := allKeys[key]; !value { 125 allKeys[key] = true 126 res = append(res, token) 127 } 128 } 129 } 130 return res 131 } 132 133 func prepareTokens(networkManager network.ManagerInterface, stores []store) []*Token { 134 tokens := make([]*Token, 0) 135 136 networks, err := networkManager.GetAll() 137 if err != nil { 138 return nil 139 } 140 141 for _, store := range stores { 142 validTokens := make([]*Token, 0) 143 for _, token := range store.GetTokens() { 144 token.Verified = true 145 146 for _, network := range networks { 147 if network.ChainID == token.ChainID { 148 validTokens = append(validTokens, token) 149 break 150 } 151 } 152 } 153 154 tokens = mergeTokens([][]*Token{tokens, validTokens}) 155 } 156 return tokens 157 } 158 159 func NewTokenManager( 160 db *sql.DB, 161 RPCClient rpc.ClientInterface, 162 communityManager *community.Manager, 163 networkManager network.ManagerInterface, 164 appDB *sql.DB, 165 mediaServer *server.MediaServer, 166 walletFeed *event.Feed, 167 accountFeed *event.Feed, 168 accountsDB *accounts.Database, 169 tokenBalancesStorage TokenBalancesStorage, 170 ) *Manager { 171 maker, _ := contracts.NewContractMaker(RPCClient) 172 stores := []store{newUniswapStore(), newDefaultStore()} 173 tokens := prepareTokens(networkManager, stores) 174 175 return &Manager{ 176 BalanceFetcher: balancefetcher.NewDefaultBalanceFetcher(maker), 177 db: db, 178 RPCClient: RPCClient, 179 ContractMaker: maker, 180 networkManager: networkManager, 181 communityManager: communityManager, 182 stores: stores, 183 communityTokensDB: communitytokensdatabase.NewCommunityTokensDatabase(appDB), 184 tokens: tokens, 185 mediaServer: mediaServer, 186 walletFeed: walletFeed, 187 accountFeed: accountFeed, 188 accountsDB: accountsDB, 189 tokenBalancesStorage: tokenBalancesStorage, 190 } 191 } 192 193 func (tm *Manager) Start() { 194 tm.startAccountsWatcher() 195 } 196 197 func (tm *Manager) startAccountsWatcher() { 198 if tm.accountWatcher != nil { 199 return 200 } 201 202 tm.accountWatcher = accountsevent.NewWatcher(tm.accountsDB, tm.accountFeed, tm.onAccountsChange) 203 tm.accountWatcher.Start() 204 } 205 206 func (tm *Manager) Stop() { 207 tm.stopAccountsWatcher() 208 } 209 210 func (tm *Manager) stopAccountsWatcher() { 211 if tm.accountWatcher != nil { 212 tm.accountWatcher.Stop() 213 tm.accountWatcher = nil 214 } 215 } 216 217 // overrideTokensInPlace overrides tokens in the store with the ones from the networks 218 // BEWARE: overridden tokens will have their original address removed and replaced by the one in networks 219 func overrideTokensInPlace(networks []params.Network, tokens []*Token) { 220 for _, network := range networks { 221 if len(network.TokenOverrides) == 0 { 222 continue 223 } 224 225 for _, overrideToken := range network.TokenOverrides { 226 for _, token := range tokens { 227 if token.Symbol == overrideToken.Symbol { 228 token.Address = overrideToken.Address 229 } 230 } 231 } 232 } 233 } 234 235 func (tm *Manager) getTokens() []*Token { 236 tm.tokenLock.RLock() 237 defer tm.tokenLock.RUnlock() 238 239 return tm.tokens 240 } 241 242 func (tm *Manager) SetTokens(tokens []*Token) { 243 tm.tokenLock.Lock() 244 defer tm.tokenLock.Unlock() 245 tm.tokens = tokens 246 } 247 248 func (tm *Manager) FindToken(network *params.Network, tokenSymbol string) *Token { 249 if tokenSymbol == network.NativeCurrencySymbol { 250 return tm.ToToken(network) 251 } 252 253 return tm.GetToken(network.ChainID, tokenSymbol) 254 } 255 256 func (tm *Manager) LookupToken(chainID *uint64, tokenSymbol string) (token *Token, isNative bool) { 257 if chainID == nil { 258 networks, err := tm.networkManager.Get(false) 259 if err != nil { 260 return nil, false 261 } 262 263 for _, network := range networks { 264 if tokenSymbol == network.NativeCurrencySymbol { 265 return tm.ToToken(network), true 266 } 267 token := tm.GetToken(network.ChainID, tokenSymbol) 268 if token != nil { 269 return token, false 270 } 271 } 272 } else { 273 network := tm.networkManager.Find(*chainID) 274 if network != nil && tokenSymbol == network.NativeCurrencySymbol { 275 return tm.ToToken(network), true 276 } 277 return tm.GetToken(*chainID, tokenSymbol), false 278 } 279 return nil, false 280 } 281 282 // GetToken returns token by chainID and tokenSymbol. Use ToToken for native token 283 func (tm *Manager) GetToken(chainID uint64, tokenSymbol string) *Token { 284 allTokens, err := tm.GetTokens(chainID) 285 if err != nil { 286 return nil 287 } 288 for _, token := range allTokens { 289 if token.Symbol == tokenSymbol { 290 return token 291 } 292 } 293 return nil 294 } 295 296 func (tm *Manager) LookupTokenIdentity(chainID uint64, address common.Address, native bool) *Token { 297 network := tm.networkManager.Find(chainID) 298 if native { 299 return tm.ToToken(network) 300 } 301 302 return tm.FindTokenByAddress(chainID, address) 303 } 304 305 func (tm *Manager) FindTokenByAddress(chainID uint64, address common.Address) *Token { 306 allTokens, err := tm.GetTokens(chainID) 307 if err != nil { 308 return nil 309 } 310 for _, token := range allTokens { 311 if token.Address == address { 312 return token 313 } 314 } 315 316 return nil 317 } 318 319 func (tm *Manager) FindOrCreateTokenByAddress(ctx context.Context, chainID uint64, address common.Address) *Token { 320 // If token comes datasource, simply returns it 321 for _, token := range tm.getTokens() { 322 if token.ChainID != chainID { 323 continue 324 } 325 if token.Address == address { 326 return token 327 } 328 } 329 330 // Create custom token if not known or try to link with a community 331 customTokens, err := tm.GetCustoms(false) 332 if err != nil { 333 return nil 334 } 335 336 for _, token := range customTokens { 337 if token.Address == address { 338 tm.discoverTokenCommunityID(ctx, token, address) 339 return token 340 } 341 } 342 343 token, err := tm.DiscoverToken(ctx, chainID, address) 344 if err != nil { 345 return nil 346 } 347 348 err = tm.UpsertCustom(*token) 349 if err != nil { 350 return nil 351 } 352 353 tm.discoverTokenCommunityID(ctx, token, address) 354 return token 355 } 356 357 func (tm *Manager) MarkAsPreviouslyOwnedToken(token *Token, owner common.Address) (bool, error) { 358 log.Info("Marking token as previously owned", "token", token, "owner", owner) 359 if token == nil { 360 return false, errors.New("token is nil") 361 } 362 if (owner == common.Address{}) { 363 return false, errors.New("owner is nil") 364 } 365 366 tokens, err := tm.tokenBalancesStorage.GetTokens() 367 if err != nil { 368 return false, err 369 } 370 371 if tokens[owner] == nil { 372 tokens[owner] = make([]StorageToken, 0) 373 } else { 374 for _, t := range tokens[owner] { 375 if t.Address == token.Address && t.ChainID == token.ChainID && t.Symbol == token.Symbol { 376 log.Info("Token already marked as previously owned", "token", token, "owner", owner) 377 return false, nil 378 } 379 } 380 } 381 382 // append token to the list of tokens 383 tokens[owner] = append(tokens[owner], StorageToken{ 384 Token: *token, 385 BalancesPerChain: map[uint64]ChainBalance{ 386 token.ChainID: { 387 RawBalance: "0", 388 Balance: &big.Float{}, 389 Address: token.Address, 390 ChainID: token.ChainID, 391 }, 392 }, 393 }) 394 395 // save the updated list of tokens 396 err = tm.tokenBalancesStorage.SaveTokens(tokens) 397 if err != nil { 398 return false, err 399 } 400 401 return true, nil 402 } 403 404 func (tm *Manager) discoverTokenCommunityID(ctx context.Context, token *Token, address common.Address) { 405 if token == nil || token.CommunityData != nil { 406 // Token is invalid or is alrady discovered. Nothing to do here. 407 return 408 } 409 backend, err := tm.RPCClient.EthClient(token.ChainID) 410 if err != nil { 411 return 412 } 413 caller, err := assets.NewAssetsCaller(address, backend) 414 if err != nil { 415 return 416 } 417 uri, err := caller.BaseTokenURI(&bind.CallOpts{ 418 Context: ctx, 419 }) 420 if err != nil { 421 return 422 } 423 424 update, err := tm.db.Prepare("UPDATE tokens SET community_id=? WHERE network_id=? AND address=?") 425 if err != nil { 426 log.Error("Cannot prepare token update query", err) 427 return 428 } 429 430 if uri == "" { 431 // Update token community ID to prevent further checks 432 _, err := update.Exec("", token.ChainID, token.Address) 433 if err != nil { 434 log.Error("Cannot update community id", err) 435 } 436 return 437 } 438 439 uri = strings.TrimSuffix(uri, "/") 440 communityIDHex, err := utils.DeserializePublicKey(uri) 441 if err != nil { 442 return 443 } 444 communityID := eth_node_types.EncodeHex(communityIDHex) 445 446 token.CommunityData = &community.Data{ 447 ID: communityID, 448 } 449 450 _, err = update.Exec(communityID, token.ChainID, token.Address) 451 if err != nil { 452 log.Error("Cannot update community id", err) 453 } 454 } 455 456 func (tm *Manager) FindSNT(chainID uint64) *Token { 457 tokens, err := tm.GetTokens(chainID) 458 if err != nil { 459 return nil 460 } 461 462 for _, token := range tokens { 463 if token.Symbol == "SNT" || token.Symbol == "STT" { 464 return token 465 } 466 } 467 468 return nil 469 } 470 471 func (tm *Manager) getNativeTokens() ([]*Token, error) { 472 tokens := make([]*Token, 0) 473 networks, err := tm.networkManager.Get(false) 474 if err != nil { 475 return nil, err 476 } 477 478 for _, network := range networks { 479 tokens = append(tokens, tm.ToToken(network)) 480 } 481 482 return tokens, nil 483 } 484 485 func (tm *Manager) GetAllTokens() ([]*Token, error) { 486 allTokens, err := tm.GetCustoms(true) 487 if err != nil { 488 log.Error("can't fetch custom tokens", "error", err) 489 } 490 491 allTokens = append(tm.getTokens(), allTokens...) 492 493 overrideTokensInPlace(tm.networkManager.GetConfiguredNetworks(), allTokens) 494 495 native, err := tm.getNativeTokens() 496 if err != nil { 497 return nil, err 498 } 499 500 allTokens = append(allTokens, native...) 501 502 return allTokens, nil 503 } 504 505 func (tm *Manager) GetTokens(chainID uint64) ([]*Token, error) { 506 tokens, err := tm.GetAllTokens() 507 if err != nil { 508 return nil, err 509 } 510 511 res := make([]*Token, 0) 512 513 for _, token := range tokens { 514 if token.ChainID == chainID { 515 res = append(res, token) 516 } 517 } 518 519 return res, nil 520 } 521 522 func (tm *Manager) GetTokensByChainIDs(chainIDs []uint64) ([]*Token, error) { 523 tokens, err := tm.GetAllTokens() 524 if err != nil { 525 return nil, err 526 } 527 528 res := make([]*Token, 0) 529 530 for _, token := range tokens { 531 for _, chainID := range chainIDs { 532 if token.ChainID == chainID { 533 res = append(res, token) 534 } 535 } 536 } 537 538 return res, nil 539 } 540 541 func (tm *Manager) GetList() *ListWrapper { 542 data := make([]*List, 0) 543 nativeTokens, err := tm.getNativeTokens() 544 if err == nil { 545 data = append(data, &List{ 546 Name: "native", 547 Tokens: nativeTokens, 548 Source: "native", 549 Version: "1.0.0", 550 }) 551 } 552 553 customTokens, err := tm.GetCustoms(true) 554 if err == nil && len(customTokens) > 0 { 555 data = append(data, &List{ 556 Name: "custom", 557 Tokens: customTokens, 558 Source: "custom", 559 Version: "1.0.0", 560 }) 561 } 562 563 updatedAt := time.Now().Unix() 564 for _, store := range tm.stores { 565 updatedAt = store.GetUpdatedAt() 566 data = append(data, &List{ 567 Name: store.GetName(), 568 Tokens: store.GetTokens(), 569 Source: store.GetSource(), 570 Version: store.GetVersion(), 571 }) 572 } 573 return &ListWrapper{ 574 Data: data, 575 UpdatedAt: updatedAt, 576 } 577 } 578 579 func (tm *Manager) DiscoverToken(ctx context.Context, chainID uint64, address common.Address) (*Token, error) { 580 caller, err := tm.ContractMaker.NewERC20(chainID, address) 581 if err != nil { 582 return nil, err 583 } 584 585 name, err := caller.Name(&bind.CallOpts{ 586 Context: ctx, 587 }) 588 if err != nil { 589 return nil, err 590 } 591 592 symbol, err := caller.Symbol(&bind.CallOpts{ 593 Context: ctx, 594 }) 595 if err != nil { 596 return nil, err 597 } 598 599 decimal, err := caller.Decimals(&bind.CallOpts{ 600 Context: ctx, 601 }) 602 if err != nil { 603 return nil, err 604 } 605 606 return &Token{ 607 Address: address, 608 Name: name, 609 Symbol: symbol, 610 Decimals: uint(decimal), 611 ChainID: chainID, 612 }, nil 613 } 614 615 func (tm *Manager) getTokensFromDB(query string, args ...any) ([]*Token, error) { 616 communityTokens := []*token.CommunityToken{} 617 if tm.communityTokensDB != nil { 618 // Error is skipped because it's only returning optional metadata 619 communityTokens, _ = tm.communityTokensDB.GetCommunityERC20Metadata() 620 } 621 622 rows, err := tm.db.Query(query, args...) 623 if err != nil { 624 return nil, err 625 } 626 defer rows.Close() 627 628 var rst []*Token 629 for rows.Next() { 630 token := &Token{} 631 var communityIDDB sql.NullString 632 err := rows.Scan(&token.Address, &token.Name, &token.Symbol, &token.Decimals, &token.ChainID, &communityIDDB) 633 if err != nil { 634 return nil, err 635 } 636 637 if communityIDDB.Valid { 638 communityID := communityIDDB.String 639 for _, communityToken := range communityTokens { 640 if communityToken.CommunityID != communityID || uint64(communityToken.ChainID) != token.ChainID || communityToken.Symbol != token.Symbol { 641 continue 642 } 643 token.Image = tm.mediaServer.MakeCommunityTokenImagesURL(communityID, token.ChainID, token.Symbol) 644 break 645 } 646 647 token.CommunityData = &community.Data{ 648 ID: communityID, 649 } 650 } 651 652 _ = tm.fillCommunityData(token) 653 654 rst = append(rst, token) 655 } 656 657 return rst, nil 658 } 659 660 func (tm *Manager) GetCustoms(onlyCommunityCustoms bool) ([]*Token, error) { 661 if onlyCommunityCustoms { 662 return tm.getTokensFromDB("SELECT address, name, symbol, decimals, network_id, community_id FROM tokens WHERE community_id IS NOT NULL AND community_id != ''") 663 } 664 return tm.getTokensFromDB("SELECT address, name, symbol, decimals, network_id, community_id FROM tokens") 665 } 666 667 func (tm *Manager) ToToken(network *params.Network) *Token { 668 return &Token{ 669 Address: common.HexToAddress("0x"), 670 Name: network.NativeCurrencyName, 671 Symbol: network.NativeCurrencySymbol, 672 Decimals: uint(network.NativeCurrencyDecimals), 673 ChainID: network.ChainID, 674 Verified: true, 675 } 676 } 677 678 func (tm *Manager) UpsertCustom(token Token) error { 679 insert, err := tm.db.Prepare("INSERT OR REPLACE INTO TOKENS (network_id, address, name, symbol, decimals) VALUES (?, ?, ?, ?, ?)") 680 if err != nil { 681 return err 682 } 683 _, err = insert.Exec(token.ChainID, token.Address, token.Name, token.Symbol, token.Decimals) 684 return err 685 } 686 687 func (tm *Manager) DeleteCustom(chainID uint64, address common.Address) error { 688 _, err := tm.db.Exec(`DELETE FROM TOKENS WHERE address = ? and network_id = ?`, address, chainID) 689 return err 690 } 691 692 func (tm *Manager) SignalCommunityTokenReceived(address common.Address, txHash common.Hash, value *big.Int, t *Token, isFirst bool) { 693 if tm.walletFeed == nil || t == nil || t.CommunityData == nil { 694 return 695 } 696 697 if len(t.CommunityData.Name) == 0 { 698 _ = tm.fillCommunityData(t) 699 } 700 if len(t.CommunityData.Name) == 0 && tm.communityManager != nil { 701 communityData, _ := tm.communityManager.FetchCommunityMetadata(t.CommunityData.ID) 702 if communityData != nil { 703 t.CommunityData.Name = communityData.CommunityName 704 t.CommunityData.Color = communityData.CommunityColor 705 t.CommunityData.Image = tm.communityManager.GetCommunityImageURL(t.CommunityData.ID) 706 } 707 } 708 709 floatAmount, _ := new(big.Float).Quo(new(big.Float).SetInt(value), new(big.Float).SetInt(new(big.Int).Exp(big.NewInt(10), big.NewInt(int64(t.Decimals)), nil))).Float64() 710 t.Image = tm.mediaServer.MakeCommunityTokenImagesURL(t.CommunityData.ID, t.ChainID, t.Symbol) 711 712 receivedToken := ReceivedToken{ 713 Token: *t, 714 Amount: floatAmount, 715 TxHash: txHash, 716 IsFirst: isFirst, 717 } 718 719 encodedMessage, err := json.Marshal(receivedToken) 720 if err != nil { 721 return 722 } 723 724 tm.walletFeed.Send(walletevent.Event{ 725 Type: EventCommunityTokenReceived, 726 ChainID: t.ChainID, 727 Accounts: []common.Address{ 728 address, 729 }, 730 Message: string(encodedMessage), 731 }) 732 } 733 734 func (tm *Manager) fillCommunityData(token *Token) error { 735 if token == nil || token.CommunityData == nil || tm.communityManager == nil { 736 return nil 737 } 738 739 communityInfo, _, err := tm.communityManager.GetCommunityInfo(token.CommunityData.ID) 740 if err != nil { 741 return err 742 } 743 if err == nil && communityInfo != nil { 744 // Fetched data from cache. Cache is refreshed during every wallet token list call. 745 token.CommunityData.Name = communityInfo.CommunityName 746 token.CommunityData.Color = communityInfo.CommunityColor 747 token.CommunityData.Image = communityInfo.CommunityImage 748 } 749 return nil 750 } 751 752 func (tm *Manager) GetTokenHistoricalBalance(account common.Address, chainID uint64, symbol string, timestamp int64) (*big.Int, error) { 753 var balance big.Int 754 err := tm.db.QueryRow("SELECT balance FROM balance_history WHERE currency = ? AND chain_id = ? AND address = ? AND timestamp < ? order by timestamp DESC LIMIT 1", symbol, chainID, account, timestamp).Scan((*bigint.SQLBigIntBytes)(&balance)) 755 if err == sql.ErrNoRows { 756 return nil, nil 757 } else if err != nil { 758 return nil, err 759 } 760 return &balance, nil 761 } 762 763 func (tm *Manager) GetPreviouslyOwnedTokens() (map[common.Address][]Token, error) { 764 storageTokens, err := tm.tokenBalancesStorage.GetTokens() 765 if err != nil { 766 return nil, err 767 } 768 769 tokens := make(map[common.Address][]Token) 770 for account, storageToken := range storageTokens { 771 for _, token := range storageToken { 772 tokens[account] = append(tokens[account], token.Token) 773 } 774 } 775 776 return tokens, nil 777 } 778 779 func (tm *Manager) removeTokenBalances(account common.Address) error { 780 _, err := tm.db.Exec("DELETE FROM token_balances WHERE user_address = ?", account.String()) 781 return err 782 } 783 784 func (tm *Manager) onAccountsChange(changedAddresses []common.Address, eventType accountsevent.EventType, currentAddresses []common.Address) { 785 if eventType == accountsevent.EventTypeRemoved { 786 for _, account := range changedAddresses { 787 err := tm.removeTokenBalances(account) 788 if err != nil { 789 log.Error("token.Manager: can't remove token balances", "error", err) 790 } 791 } 792 } 793 } 794 795 func (tm *Manager) GetCachedBalancesByChain(accounts, tokenAddresses []common.Address, chainIDs []uint64) (map[uint64]map[common.Address]map[common.Address]*hexutil.Big, error) { 796 accountStrings := make([]string, len(accounts)) 797 for i, account := range accounts { 798 accountStrings[i] = fmt.Sprintf("'%s'", account.Hex()) 799 } 800 801 tokenAddressStrings := make([]string, len(tokenAddresses)) 802 for i, tokenAddress := range tokenAddresses { 803 tokenAddressStrings[i] = fmt.Sprintf("'%s'", tokenAddress.Hex()) 804 } 805 806 chainIDStrings := make([]string, len(chainIDs)) 807 for i, chainID := range chainIDs { 808 chainIDStrings[i] = fmt.Sprintf("%d", chainID) 809 } 810 811 query := `SELECT chain_id, user_address, token_address, raw_balance 812 FROM token_balances 813 WHERE user_address IN (` + strings.Join(accountStrings, ",") + `) 814 AND token_address IN (` + strings.Join(tokenAddressStrings, ",") + `) 815 AND chain_id IN (` + strings.Join(chainIDStrings, ",") + `)` 816 817 rows, err := tm.db.Query(query) 818 if err != nil { 819 return nil, err 820 } 821 defer rows.Close() 822 823 ret := make(map[uint64]map[common.Address]map[common.Address]*hexutil.Big) 824 825 for rows.Next() { 826 var chainID uint64 827 var userAddressStr, tokenAddressStr string 828 var rawBalance string 829 830 err := rows.Scan(&chainID, &userAddressStr, &tokenAddressStr, &rawBalance) 831 if err != nil { 832 return nil, err 833 } 834 835 num := new(hexutil.Big) 836 _, ok := num.ToInt().SetString(rawBalance, 10) 837 if !ok { 838 return ret, nil 839 } 840 841 if ret[chainID] == nil { 842 ret[chainID] = make(map[common.Address]map[common.Address]*hexutil.Big) 843 } 844 845 if ret[chainID][common.HexToAddress(userAddressStr)] == nil { 846 ret[chainID][common.HexToAddress(userAddressStr)] = make(map[common.Address]*hexutil.Big) 847 } 848 849 ret[chainID][common.HexToAddress(userAddressStr)][common.HexToAddress(tokenAddressStr)] = num 850 } 851 852 return ret, nil 853 }