status-go/services/wallet/token/token.go

967 lines
26 KiB
Go
Raw Normal View History

2022-09-13 07:10:59 +00:00
package token
import (
"context"
"database/sql"
"encoding/json"
"errors"
"math/big"
"strconv"
2023-10-25 16:49:18 +00:00
"strings"
"sync"
"time"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/event"
"github.com/ethereum/go-ethereum/log"
2022-09-09 06:58:36 +00:00
"github.com/status-im/status-go/contracts"
2023-10-25 16:49:18 +00:00
"github.com/status-im/status-go/contracts/community-tokens/assets"
"github.com/status-im/status-go/contracts/ethscan"
2022-02-02 22:50:55 +00:00
"github.com/status-im/status-go/contracts/ierc20"
2023-10-25 16:49:18 +00:00
eth_node_types "github.com/status-im/status-go/eth-node/types"
"github.com/status-im/status-go/multiaccounts/accounts"
2022-09-13 07:10:59 +00:00
"github.com/status-im/status-go/params"
"github.com/status-im/status-go/protocol/communities/token"
"github.com/status-im/status-go/rpc"
2023-02-20 09:32:45 +00:00
"github.com/status-im/status-go/rpc/chain"
2022-04-13 07:55:38 +00:00
"github.com/status-im/status-go/rpc/network"
"github.com/status-im/status-go/server"
"github.com/status-im/status-go/services/accounts/accountsevent"
"github.com/status-im/status-go/services/communitytokens/communitytokensdatabase"
2023-10-25 16:49:18 +00:00
"github.com/status-im/status-go/services/utils"
"github.com/status-im/status-go/services/wallet/async"
"github.com/status-im/status-go/services/wallet/bigint"
"github.com/status-im/status-go/services/wallet/community"
"github.com/status-im/status-go/services/wallet/walletevent"
)
const (
EventCommunityTokenReceived walletevent.EventType = "wallet-community-token-received"
)
var requestTimeout = 20 * time.Second
2022-09-09 06:58:36 +00:00
var nativeChainAddress = common.HexToAddress("0x")
type Token struct {
Address common.Address `json:"address"`
Name string `json:"name"`
Symbol string `json:"symbol"`
// Decimals defines how divisible the token is. For example, 0 would be
// indivisible, whereas 18 would allow very small amounts of the token
// to be traded.
Decimals uint `json:"decimals"`
ChainID uint64 `json:"chainId"`
// PegSymbol indicates that the token is pegged to some fiat currency, using the
// ISO 4217 alphabetic code. For example, an empty string means it is not
// pegged, while "USD" means it's pegged to the United States Dollar.
PegSymbol string `json:"pegSymbol"`
Image string `json:"image,omitempty"`
2023-08-31 07:47:24 +00:00
CommunityData *community.Data `json:"community_data,omitempty"`
Verified bool `json:"verified"`
TokenListID string `json:"tokenListId"`
}
type ReceivedToken struct {
Token
Amount float64 `json:"amount"`
TxHash common.Hash `json:"txHash"`
IsFirst bool `json:"isFirst"`
}
2022-09-13 07:10:59 +00:00
func (t *Token) IsNative() bool {
2024-04-01 13:39:17 +00:00
return strings.EqualFold(t.Symbol, "ETH")
2022-09-13 07:10:59 +00:00
}
2023-10-17 15:05:05 +00:00
type List struct {
2024-02-01 12:00:06 +00:00
Name string `json:"name"`
Tokens []*Token `json:"tokens"`
Source string `json:"source"`
Version string `json:"version"`
}
type ListWrapper struct {
UpdatedAt int64 `json:"updatedAt"`
Data []*List `json:"data"`
2023-10-17 15:05:05 +00:00
}
type addressTokenMap = map[common.Address]*Token
type storeMap = map[uint64]addressTokenMap
type ManagerInterface interface {
LookupTokenIdentity(chainID uint64, address common.Address, native bool) *Token
LookupToken(chainID *uint64, tokenSymbol string) (token *Token, isNative bool)
}
// Manager is used for accessing token store. It changes the token store based on overridden tokens
2022-10-25 14:50:32 +00:00
type Manager struct {
db *sql.DB
RPCClient *rpc.Client
ContractMaker *contracts.ContractMaker
networkManager *network.Manager
stores []store // Set on init, not changed afterwards
communityTokensDB *communitytokensdatabase.Database
communityManager *community.Manager
mediaServer *server.MediaServer
walletFeed *event.Feed
accountFeed *event.Feed
accountWatcher *accountsevent.Watcher
accountsDB *accounts.Database
2023-09-11 14:44:43 +00:00
2023-10-17 15:05:05 +00:00
tokens []*Token
2023-09-11 14:44:43 +00:00
tokenLock sync.RWMutex
2022-04-13 07:55:38 +00:00
}
2023-10-17 15:05:05 +00:00
func mergeTokens(sliceLists [][]*Token) []*Token {
allKeys := make(map[string]bool)
res := []*Token{}
for _, list := range sliceLists {
for _, token := range list {
key := strconv.FormatUint(token.ChainID, 10) + token.Address.String()
if _, value := allKeys[key]; !value {
allKeys[key] = true
res = append(res, token)
}
}
}
return res
}
func prepareTokens(networkManager *network.Manager, stores []store) []*Token {
2023-10-17 15:05:05 +00:00
tokens := make([]*Token, 0)
2023-10-17 15:05:05 +00:00
networks, err := networkManager.GetAll()
2023-03-28 13:15:34 +00:00
if err != nil {
2023-10-17 15:05:05 +00:00
return nil
2023-03-28 13:15:34 +00:00
}
2023-10-17 15:05:05 +00:00
for _, store := range stores {
2023-03-28 13:15:34 +00:00
validTokens := make([]*Token, 0)
2023-10-17 15:05:05 +00:00
for _, token := range store.GetTokens() {
2023-08-31 07:47:24 +00:00
token.Verified = true
2023-03-28 13:15:34 +00:00
for _, network := range networks {
if network.ChainID == token.ChainID {
validTokens = append(validTokens, token)
break
}
}
}
2023-10-17 15:05:05 +00:00
tokens = mergeTokens([][]*Token{tokens, validTokens})
}
return tokens
}
func NewTokenManager(
db *sql.DB,
RPCClient *rpc.Client,
communityManager *community.Manager,
networkManager *network.Manager,
appDB *sql.DB,
mediaServer *server.MediaServer,
walletFeed *event.Feed,
accountFeed *event.Feed,
accountsDB *accounts.Database,
) *Manager {
maker, _ := contracts.NewContractMaker(RPCClient)
stores := []store{newUniswapStore(), newDefaultStore()}
tokens := prepareTokens(networkManager, stores)
2023-09-11 14:44:43 +00:00
2023-10-17 15:05:05 +00:00
return &Manager{
db: db,
RPCClient: RPCClient,
ContractMaker: maker,
networkManager: networkManager,
communityManager: communityManager,
stores: stores,
communityTokensDB: communitytokensdatabase.NewCommunityTokensDatabase(appDB),
tokens: tokens,
mediaServer: mediaServer,
walletFeed: walletFeed,
accountFeed: accountFeed,
accountsDB: accountsDB,
}
}
func (tm *Manager) Start() {
tm.startAccountsWatcher()
}
func (tm *Manager) startAccountsWatcher() {
if tm.accountWatcher != nil {
return
}
tm.accountWatcher = accountsevent.NewWatcher(tm.accountsDB, tm.accountFeed, tm.onAccountsChange)
tm.accountWatcher.Start()
}
func (tm *Manager) Stop() {
tm.stopAccountsWatcher()
}
func (tm *Manager) stopAccountsWatcher() {
if tm.accountWatcher != nil {
tm.accountWatcher.Stop()
tm.accountWatcher = nil
2023-10-17 15:05:05 +00:00
}
}
2023-10-17 15:05:05 +00:00
// overrideTokensInPlace overrides tokens in the store with the ones from the networks
// BEWARE: overridden tokens will have their original address removed and replaced by the one in networks
func overrideTokensInPlace(networks []params.Network, tokens []*Token) {
for _, network := range networks {
if len(network.TokenOverrides) == 0 {
continue
}
2023-10-17 15:05:05 +00:00
for _, overrideToken := range network.TokenOverrides {
for _, token := range tokens {
if token.Symbol == overrideToken.Symbol {
token.Address = overrideToken.Address
}
}
}
2022-09-13 07:10:59 +00:00
}
2023-10-17 15:05:05 +00:00
}
func (tm *Manager) getTokens() []*Token {
tm.tokenLock.RLock()
defer tm.tokenLock.RUnlock()
return tm.tokens
}
2023-10-17 15:05:05 +00:00
func (tm *Manager) SetTokens(tokens []*Token) {
tm.tokenLock.Lock()
defer tm.tokenLock.Unlock()
tm.tokens = tokens
}
func (tm *Manager) FindToken(network *params.Network, tokenSymbol string) *Token {
if tokenSymbol == network.NativeCurrencySymbol {
return tm.ToToken(network)
}
return tm.GetToken(network.ChainID, tokenSymbol)
}
func (tm *Manager) LookupToken(chainID *uint64, tokenSymbol string) (token *Token, isNative bool) {
if chainID == nil {
2023-10-17 15:05:05 +00:00
networks, err := tm.networkManager.Get(false)
if err != nil {
return nil, false
}
for _, network := range networks {
if tokenSymbol == network.NativeCurrencySymbol {
return tm.ToToken(network), true
}
token := tm.GetToken(network.ChainID, tokenSymbol)
if token != nil {
return token, false
}
}
} else {
network := tm.networkManager.Find(*chainID)
if network != nil && tokenSymbol == network.NativeCurrencySymbol {
return tm.ToToken(network), true
}
return tm.GetToken(*chainID, tokenSymbol), false
}
return nil, false
}
// GetToken returns token by chainID and tokenSymbol. Use ToToken for native token
func (tm *Manager) GetToken(chainID uint64, tokenSymbol string) *Token {
2023-10-17 15:05:05 +00:00
allTokens, err := tm.GetTokens(chainID)
if err != nil {
return nil
}
2022-09-13 07:10:59 +00:00
for _, token := range allTokens {
if token.Symbol == tokenSymbol {
return token
}
}
return nil
}
func (tm *Manager) LookupTokenIdentity(chainID uint64, address common.Address, native bool) *Token {
network := tm.networkManager.Find(chainID)
if native {
return tm.ToToken(network)
}
return tm.FindTokenByAddress(chainID, address)
}
func (tm *Manager) FindTokenByAddress(chainID uint64, address common.Address) *Token {
2023-10-17 15:05:05 +00:00
allTokens, err := tm.GetTokens(chainID)
if err != nil {
return nil
}
for _, token := range allTokens {
if token.Address == address {
return token
}
}
2023-08-31 07:47:24 +00:00
return nil
}
2023-08-31 07:47:24 +00:00
func (tm *Manager) FindOrCreateTokenByAddress(ctx context.Context, chainID uint64, address common.Address) *Token {
2023-10-17 15:05:05 +00:00
// If token comes datasource, simply returns it
for _, token := range tm.getTokens() {
if token.ChainID != chainID {
continue
}
if token.Address == address {
return token
}
}
// Create custom token if not known or try to link with a community
customTokens, err := tm.GetCustoms(false)
if err != nil {
return nil
}
for _, token := range customTokens {
2023-08-31 07:47:24 +00:00
if token.Address == address {
tm.discoverTokenCommunityID(ctx, token, address)
2023-08-31 07:47:24 +00:00
return token
}
}
token, err := tm.DiscoverToken(ctx, chainID, address)
if err != nil {
return nil
}
err = tm.UpsertCustom(*token)
if err != nil {
return nil
}
tm.discoverTokenCommunityID(ctx, token, address)
2023-08-31 07:47:24 +00:00
return token
}
func (tm *Manager) MarkAsPreviouslyOwnedToken(token *Token, owner common.Address) (bool, error) {
log.Info("Marking token as previously owned", "token", token, "owner", owner)
if token == nil {
return false, errors.New("token is nil")
}
if (owner == common.Address{}) {
return false, errors.New("owner is nil")
}
count := 0
err := tm.db.QueryRow(`SELECT EXISTS(SELECT 1 FROM token_balances WHERE user_address = ? AND token_address = ? AND chain_id = ?)`, owner.Hex(), token.Address.Hex(), token.ChainID).Scan(&count)
if err != nil || count > 0 {
return false, err
}
_, err = tm.db.Exec(`INSERT INTO token_balances(user_address,token_name,token_symbol,token_address,token_decimals,chain_id,token_decimals,raw_balance,balance) VALUES (?,?,?,?,?,?,?,?,?)`, owner.Hex(), token.Name, token.Symbol, token.Address.Hex(), token.Decimals, token.ChainID, 0, "0", "0")
return true, err
}
2023-10-25 16:49:18 +00:00
func (tm *Manager) discoverTokenCommunityID(ctx context.Context, token *Token, address common.Address) {
if token == nil || token.CommunityData != nil {
2023-10-25 16:49:18 +00:00
// Token is invalid or is alrady discovered. Nothing to do here.
return
}
backend, err := tm.RPCClient.EthClient(token.ChainID)
if err != nil {
return
}
caller, err := assets.NewAssetsCaller(address, backend)
if err != nil {
return
}
uri, err := caller.BaseTokenURI(&bind.CallOpts{
Context: ctx,
})
if err != nil {
return
}
update, err := tm.db.Prepare("UPDATE tokens SET community_id=? WHERE network_id=? AND address=?")
if err != nil {
log.Error("Cannot prepare token update query", err)
return
}
if uri == "" {
// Update token community ID to prevent further checks
_, err := update.Exec("", token.ChainID, token.Address)
if err != nil {
log.Error("Cannot update community id", err)
}
return
}
uri = strings.TrimSuffix(uri, "/")
communityIDHex, err := utils.DeserializePublicKey(uri)
if err != nil {
return
}
communityID := eth_node_types.EncodeHex(communityIDHex)
token.CommunityData = &community.Data{
ID: communityID,
}
2023-10-25 16:49:18 +00:00
_, err = update.Exec(communityID, token.ChainID, token.Address)
if err != nil {
log.Error("Cannot update community id", err)
}
}
2022-10-25 14:50:32 +00:00
func (tm *Manager) FindSNT(chainID uint64) *Token {
2023-10-17 15:05:05 +00:00
tokens, err := tm.GetTokens(chainID)
if err != nil {
2022-04-13 07:55:38 +00:00
return nil
}
for _, token := range tokens {
2022-04-13 07:55:38 +00:00
if token.Symbol == "SNT" || token.Symbol == "STT" {
return token
}
}
return nil
}
2023-10-17 15:05:05 +00:00
func (tm *Manager) getNativeTokens() ([]*Token, error) {
tokens := make([]*Token, 0)
2023-08-31 07:47:24 +00:00
networks, err := tm.networkManager.Get(false)
if err != nil {
return nil, err
}
for _, network := range networks {
2023-10-17 15:05:05 +00:00
tokens = append(tokens, tm.ToToken(network))
}
2023-10-17 15:05:05 +00:00
return tokens, nil
}
2022-10-27 07:38:05 +00:00
func (tm *Manager) GetAllTokens() ([]*Token, error) {
2023-10-17 15:05:05 +00:00
allTokens, err := tm.GetCustoms(true)
2022-10-27 07:38:05 +00:00
if err != nil {
log.Error("can't fetch custom tokens", "error", err)
2022-10-27 07:38:05 +00:00
}
2023-01-13 17:12:46 +00:00
2023-10-17 15:05:05 +00:00
allTokens = append(tm.getTokens(), allTokens...)
2022-10-27 07:38:05 +00:00
2023-10-17 15:05:05 +00:00
overrideTokensInPlace(tm.networkManager.GetConfiguredNetworks(), allTokens)
2023-10-17 15:05:05 +00:00
native, err := tm.getNativeTokens()
if err != nil {
return nil, err
}
allTokens = append(allTokens, native...)
return allTokens, nil
2022-10-27 07:38:05 +00:00
}
2023-10-17 15:05:05 +00:00
func (tm *Manager) GetTokens(chainID uint64) ([]*Token, error) {
tokens, err := tm.GetAllTokens()
if err != nil {
return nil, err
}
2023-10-17 15:05:05 +00:00
res := make([]*Token, 0)
for _, token := range tokens {
if token.ChainID == chainID {
res = append(res, token)
}
}
2023-10-17 15:05:05 +00:00
return res, nil
}
func (tm *Manager) GetTokensByChainIDs(chainIDs []uint64) ([]*Token, error) {
tokens, err := tm.GetAllTokens()
if err != nil {
return nil, err
2022-01-14 09:21:00 +00:00
}
2023-10-17 15:05:05 +00:00
res := make([]*Token, 0)
2022-01-14 09:21:00 +00:00
2023-10-17 15:05:05 +00:00
for _, token := range tokens {
for _, chainID := range chainIDs {
if token.ChainID == chainID {
res = append(res, token)
}
}
2022-01-14 09:21:00 +00:00
}
2023-10-17 15:05:05 +00:00
return res, nil
}
2024-02-01 12:00:06 +00:00
func (tm *Manager) GetList() *ListWrapper {
data := make([]*List, 0)
2023-10-17 15:05:05 +00:00
nativeTokens, err := tm.getNativeTokens()
if err == nil {
2024-02-01 12:00:06 +00:00
data = append(data, &List{
Name: "native",
Tokens: nativeTokens,
Source: "native",
Version: "1.0.0",
2023-10-17 15:05:05 +00:00
})
}
2022-01-14 09:21:00 +00:00
2023-10-17 15:05:05 +00:00
customTokens, err := tm.GetCustoms(true)
if err == nil && len(customTokens) > 0 {
2024-02-01 12:00:06 +00:00
data = append(data, &List{
Name: "custom",
Tokens: customTokens,
Source: "custom",
Version: "1.0.0",
2023-10-17 15:05:05 +00:00
})
}
2024-02-01 12:00:06 +00:00
updatedAt := time.Now().Unix()
2023-10-17 15:05:05 +00:00
for _, store := range tm.stores {
2024-02-01 12:00:06 +00:00
updatedAt = store.GetUpdatedAt()
data = append(data, &List{
Name: store.GetName(),
Tokens: store.GetTokens(),
Source: store.GetSource(),
Version: store.GetVersion(),
2023-10-17 15:05:05 +00:00
})
}
2024-02-01 12:00:06 +00:00
return &ListWrapper{
Data: data,
UpdatedAt: updatedAt,
}
2022-01-14 09:21:00 +00:00
}
2022-10-25 14:50:32 +00:00
func (tm *Manager) DiscoverToken(ctx context.Context, chainID uint64, address common.Address) (*Token, error) {
caller, err := tm.ContractMaker.NewERC20(chainID, address)
if err != nil {
return nil, err
}
name, err := caller.Name(&bind.CallOpts{
Context: ctx,
})
if err != nil {
return nil, err
}
symbol, err := caller.Symbol(&bind.CallOpts{
Context: ctx,
})
if err != nil {
return nil, err
}
decimal, err := caller.Decimals(&bind.CallOpts{
Context: ctx,
})
if err != nil {
return nil, err
}
return &Token{
Address: address,
Name: name,
Symbol: symbol,
Decimals: uint(decimal),
2023-08-31 07:47:24 +00:00
ChainID: chainID,
}, nil
}
2023-10-17 15:05:05 +00:00
func (tm *Manager) getTokensFromDB(query string, args ...any) ([]*Token, error) {
communityTokens := []*token.CommunityToken{}
if tm.communityTokensDB != nil {
// Error is skipped because it's only returning optional metadata
communityTokens, _ = tm.communityTokensDB.GetCommunityERC20Metadata()
}
2023-10-25 16:49:18 +00:00
rows, err := tm.db.Query(query, args...)
if err != nil {
return nil, err
}
defer rows.Close()
var rst []*Token
for rows.Next() {
token := &Token{}
2023-10-25 16:49:18 +00:00
var communityIDDB sql.NullString
2023-10-17 15:05:05 +00:00
err := rows.Scan(&token.Address, &token.Name, &token.Symbol, &token.Decimals, &token.ChainID, &communityIDDB)
if err != nil {
return nil, err
}
2023-10-25 16:49:18 +00:00
if communityIDDB.Valid {
communityID := communityIDDB.String
for _, communityToken := range communityTokens {
if communityToken.CommunityID != communityID || uint64(communityToken.ChainID) != token.ChainID || communityToken.Symbol != token.Symbol {
continue
}
token.Image = tm.mediaServer.MakeCommunityTokenImagesURL(communityID, token.ChainID, token.Symbol)
break
}
token.CommunityData = &community.Data{
ID: communityID,
}
2023-10-25 16:49:18 +00:00
}
_ = tm.fillCommunityData(token)
rst = append(rst, token)
}
return rst, nil
}
2023-10-17 15:05:05 +00:00
func (tm *Manager) GetCustoms(onlyCommunityCustoms bool) ([]*Token, error) {
if onlyCommunityCustoms {
2023-10-17 15:05:05 +00:00
return tm.getTokensFromDB("SELECT address, name, symbol, decimals, network_id, community_id FROM tokens WHERE community_id IS NOT NULL AND community_id != ''")
2022-04-13 07:55:38 +00:00
}
2023-10-17 15:05:05 +00:00
return tm.getTokensFromDB("SELECT address, name, symbol, decimals, network_id, community_id FROM tokens")
2022-04-13 07:55:38 +00:00
}
2022-10-27 07:38:05 +00:00
func (tm *Manager) ToToken(network *params.Network) *Token {
return &Token{
Address: common.HexToAddress("0x"),
Name: network.NativeCurrencyName,
Symbol: network.NativeCurrencySymbol,
Decimals: uint(network.NativeCurrencyDecimals),
ChainID: network.ChainID,
2023-08-31 07:47:24 +00:00
Verified: true,
2022-10-27 07:38:05 +00:00
}
}
2022-10-25 14:50:32 +00:00
func (tm *Manager) UpsertCustom(token Token) error {
2023-10-17 15:05:05 +00:00
insert, err := tm.db.Prepare("INSERT OR REPLACE INTO TOKENS (network_id, address, name, symbol, decimals) VALUES (?, ?, ?, ?, ?)")
if err != nil {
return err
}
2023-10-17 15:05:05 +00:00
_, err = insert.Exec(token.ChainID, token.Address, token.Name, token.Symbol, token.Decimals)
return err
}
2022-10-25 14:50:32 +00:00
func (tm *Manager) DeleteCustom(chainID uint64, address common.Address) error {
_, err := tm.db.Exec(`DELETE FROM TOKENS WHERE address = ? and network_id = ?`, address, chainID)
return err
}
func (tm *Manager) GetTokenBalance(ctx context.Context, client chain.ClientInterface, account common.Address, token common.Address) (*big.Int, error) {
2022-05-10 07:48:05 +00:00
caller, err := ierc20.NewIERC20Caller(token, client)
if err != nil {
return nil, err
}
return caller.BalanceOf(&bind.CallOpts{
Context: ctx,
}, account)
}
func (tm *Manager) GetTokenBalanceAt(ctx context.Context, client chain.ClientInterface, account common.Address, token common.Address, blockNumber *big.Int) (*big.Int, error) {
feat: retrieve balance history for tokens and cache it to DB Extends wallet module with the history package with the following components: BalanceDB (balance_db.go) - Keeps track of balance information (token count, block, block timestamp) for a token identity (chain, address, currency) - The cached data is stored in `balance_history` table. - Uniqueness constrained is enforced by the `balance_history_identify_entry` UNIQUE index. - Optimal DB fetching is ensured by the `balance_history_filter_entries` index Balance (balance.go) - Provides two stages: - Fetch of balance history using RPC calls (Balance.update function) - Retrieving of cached balance data from the DB it exists (Balance.get function) - Fetching and retrieving of data is done for specific time intervals defined by TimeInterval "enumeration" - Update process is done for a token identity by the Balance.Update function - The granularity of data points returned is defined by the constant increment step define in `timeIntervalToStride` for each time interval. - The `blocksStride` values have a common divisor to have cache hit between time intervals. Service (service.go) - Main APIs - StartBalanceHistory: Regularly updates balance history for all enabled networks, available accounts and provided tokens. - GetBalanceHistory: retrieves cached token count for a token identity (chain, address, currency) for multiple chains - UpdateVisibleTokens: will set the list of tokens to have historical balance fetched. This is a simplification to limit tokens to a small list that make sense Fetch balance history for ECR20 tokens - Add token.Manager.GetTokenBalanceAt to fetch balance of a specific block number of ECR20. - Add tokenChainClientSource concrete implementation of DataSource to fetch balance of ECR20 tokens. - Chose the correct DataSource implementation based on the token "is native" property. Tests Tests are implemented using a mock of `DataSource` interface used to intercept the RPC calls. Notes: - the timestamp used for retrieving block balance is constant Closes status-desktop: #8175, #8226, #8862
2022-11-15 12:14:41 +00:00
caller, err := ierc20.NewIERC20Caller(token, client)
if err != nil {
return nil, err
}
balance, err := caller.BalanceOf(&bind.CallOpts{
feat: retrieve balance history for tokens and cache it to DB Extends wallet module with the history package with the following components: BalanceDB (balance_db.go) - Keeps track of balance information (token count, block, block timestamp) for a token identity (chain, address, currency) - The cached data is stored in `balance_history` table. - Uniqueness constrained is enforced by the `balance_history_identify_entry` UNIQUE index. - Optimal DB fetching is ensured by the `balance_history_filter_entries` index Balance (balance.go) - Provides two stages: - Fetch of balance history using RPC calls (Balance.update function) - Retrieving of cached balance data from the DB it exists (Balance.get function) - Fetching and retrieving of data is done for specific time intervals defined by TimeInterval "enumeration" - Update process is done for a token identity by the Balance.Update function - The granularity of data points returned is defined by the constant increment step define in `timeIntervalToStride` for each time interval. - The `blocksStride` values have a common divisor to have cache hit between time intervals. Service (service.go) - Main APIs - StartBalanceHistory: Regularly updates balance history for all enabled networks, available accounts and provided tokens. - GetBalanceHistory: retrieves cached token count for a token identity (chain, address, currency) for multiple chains - UpdateVisibleTokens: will set the list of tokens to have historical balance fetched. This is a simplification to limit tokens to a small list that make sense Fetch balance history for ECR20 tokens - Add token.Manager.GetTokenBalanceAt to fetch balance of a specific block number of ECR20. - Add tokenChainClientSource concrete implementation of DataSource to fetch balance of ECR20 tokens. - Chose the correct DataSource implementation based on the token "is native" property. Tests Tests are implemented using a mock of `DataSource` interface used to intercept the RPC calls. Notes: - the timestamp used for retrieving block balance is constant Closes status-desktop: #8175, #8226, #8862
2022-11-15 12:14:41 +00:00
Context: ctx,
BlockNumber: blockNumber,
}, account)
if err != nil {
if err != bind.ErrNoCode {
return nil, err
}
balance = big.NewInt(0)
}
return balance, nil
feat: retrieve balance history for tokens and cache it to DB Extends wallet module with the history package with the following components: BalanceDB (balance_db.go) - Keeps track of balance information (token count, block, block timestamp) for a token identity (chain, address, currency) - The cached data is stored in `balance_history` table. - Uniqueness constrained is enforced by the `balance_history_identify_entry` UNIQUE index. - Optimal DB fetching is ensured by the `balance_history_filter_entries` index Balance (balance.go) - Provides two stages: - Fetch of balance history using RPC calls (Balance.update function) - Retrieving of cached balance data from the DB it exists (Balance.get function) - Fetching and retrieving of data is done for specific time intervals defined by TimeInterval "enumeration" - Update process is done for a token identity by the Balance.Update function - The granularity of data points returned is defined by the constant increment step define in `timeIntervalToStride` for each time interval. - The `blocksStride` values have a common divisor to have cache hit between time intervals. Service (service.go) - Main APIs - StartBalanceHistory: Regularly updates balance history for all enabled networks, available accounts and provided tokens. - GetBalanceHistory: retrieves cached token count for a token identity (chain, address, currency) for multiple chains - UpdateVisibleTokens: will set the list of tokens to have historical balance fetched. This is a simplification to limit tokens to a small list that make sense Fetch balance history for ECR20 tokens - Add token.Manager.GetTokenBalanceAt to fetch balance of a specific block number of ECR20. - Add tokenChainClientSource concrete implementation of DataSource to fetch balance of ECR20 tokens. - Chose the correct DataSource implementation based on the token "is native" property. Tests Tests are implemented using a mock of `DataSource` interface used to intercept the RPC calls. Notes: - the timestamp used for retrieving block balance is constant Closes status-desktop: #8175, #8226, #8862
2022-11-15 12:14:41 +00:00
}
func (tm *Manager) GetChainBalance(ctx context.Context, client chain.ClientInterface, account common.Address) (*big.Int, error) {
2022-05-10 07:48:05 +00:00
return client.BalanceAt(ctx, account, nil)
}
func (tm *Manager) GetBalance(ctx context.Context, client chain.ClientInterface, account common.Address, token common.Address) (*big.Int, error) {
2022-09-09 06:58:36 +00:00
if token == nativeChainAddress {
2022-09-13 07:10:59 +00:00
return tm.GetChainBalance(ctx, client, account)
}
2022-09-13 07:10:59 +00:00
return tm.GetTokenBalance(ctx, client, account, token)
}
func (tm *Manager) GetBalancesByChain(parent context.Context, clients map[uint64]chain.ClientInterface, accounts, tokens []common.Address) (map[uint64]map[common.Address]map[common.Address]*hexutil.Big, error) {
return tm.GetBalancesAtByChain(parent, clients, accounts, tokens, nil)
}
func (tm *Manager) GetBalancesAtByChain(parent context.Context, clients map[uint64]chain.ClientInterface, accounts, tokens []common.Address, atBlocks map[uint64]*big.Int) (map[uint64]map[common.Address]map[common.Address]*hexutil.Big, error) {
2022-10-27 07:38:05 +00:00
var (
group = async.NewAtomicGroup(parent)
mu sync.Mutex
response = map[uint64]map[common.Address]map[common.Address]*hexutil.Big{}
)
updateBalance := func(chainID uint64, account common.Address, token common.Address, balance *big.Int) {
mu.Lock()
if _, ok := response[chainID]; !ok {
response[chainID] = map[common.Address]map[common.Address]*hexutil.Big{}
}
if _, ok := response[chainID][account]; !ok {
response[chainID][account] = map[common.Address]*hexutil.Big{}
}
if _, ok := response[chainID][account][token]; !ok {
zeroHex := hexutil.Big(*big.NewInt(0))
response[chainID][account][token] = &zeroHex
}
sum := big.NewInt(0).Add(response[chainID][account][token].ToInt(), balance)
sumHex := hexutil.Big(*sum)
response[chainID][account][token] = &sumHex
mu.Unlock()
}
2023-10-05 11:55:28 +00:00
for clientIdx := range clients {
// Keep the reference to the client. DO NOT USE A LOOP, the client will be overridden in the coroutine
client := clients[clientIdx]
ethScanContract, availableAtBlock, err := tm.ContractMaker.NewEthScan(client.NetworkID())
2023-03-30 07:06:47 +00:00
if err != nil {
log.Error("error scanning contract", "err", err)
2023-03-30 07:06:47 +00:00
return nil, err
}
2022-10-27 07:38:05 +00:00
atBlock := atBlocks[client.NetworkID()]
2023-03-30 07:06:47 +00:00
fetchChainBalance := false
var tokenChunks [][]common.Address
chunkSize := 500
for i := 0; i < len(tokens); i += chunkSize {
end := i + chunkSize
if end > len(tokens) {
end = len(tokens)
2022-10-27 07:38:05 +00:00
}
2023-03-30 07:06:47 +00:00
tokenChunks = append(tokenChunks, tokens[i:end])
}
for _, token := range tokens {
if token == nativeChainAddress {
fetchChainBalance = true
2022-10-27 07:38:05 +00:00
}
2023-03-30 07:06:47 +00:00
}
if fetchChainBalance {
group.Add(func(parent context.Context) error {
ctx, cancel := context.WithTimeout(parent, requestTimeout)
defer cancel()
res, err := ethScanContract.EtherBalances(&bind.CallOpts{
Context: ctx,
BlockNumber: atBlock,
2023-03-30 07:06:47 +00:00
}, accounts)
if err != nil {
log.Error("can't fetch chain balance 5", "err", err)
2023-03-30 07:06:47 +00:00
return nil
}
for idx, account := range accounts {
balance := new(big.Int)
balance.SetBytes(res[idx].Data)
updateBalance(client.NetworkID(), account, common.HexToAddress("0x"), balance)
2023-03-30 07:06:47 +00:00
}
return nil
})
}
for accountIdx := range accounts {
2023-10-05 11:55:28 +00:00
// Keep the reference to the account. DO NOT USE A LOOP, the account will be overridden in the coroutine
2023-03-30 07:06:47 +00:00
account := accounts[accountIdx]
for idx := range tokenChunks {
2023-10-05 11:55:28 +00:00
// Keep the reference to the chunk. DO NOT USE A LOOP, the chunk will be overridden in the coroutine
2023-03-30 07:06:47 +00:00
chunk := tokenChunks[idx]
2023-10-05 11:55:28 +00:00
2022-10-27 07:38:05 +00:00
group.Add(func(parent context.Context) error {
ctx, cancel := context.WithTimeout(parent, requestTimeout)
defer cancel()
var res []ethscan.BalanceScannerResult
if atBlock == nil || big.NewInt(int64(availableAtBlock)).Cmp(atBlock) < 0 {
res, err = ethScanContract.TokensBalance(&bind.CallOpts{
Context: ctx,
BlockNumber: atBlock,
}, account, chunk)
if err != nil {
log.Error("can't fetch erc20 token balance 6", "account", account, "error", err)
return nil
}
if len(res) != len(chunk) {
log.Error("can't fetch erc20 token balance 7", "account", account, "error", "response not complete")
return nil
}
2023-03-30 07:06:47 +00:00
for idx, token := range chunk {
if !res[idx].Success {
continue
}
balance := new(big.Int)
balance.SetBytes(res[idx].Data)
updateBalance(client.NetworkID(), account, token, balance)
}
return nil
}
for _, token := range chunk {
balance, err := tm.GetTokenBalanceAt(ctx, client, account, token, atBlock)
if err != nil {
if err != bind.ErrNoCode {
log.Error("can't fetch erc20 token balance 8", "account", account, "token", token, "error", "on fetching token balance")
return nil
}
2023-03-30 07:06:47 +00:00
}
updateBalance(client.NetworkID(), account, token, balance)
2022-10-27 07:38:05 +00:00
}
2022-10-27 07:38:05 +00:00
return nil
})
}
}
}
select {
case <-group.WaitAsync():
case <-parent.Done():
return nil, parent.Err()
}
return response, group.Error()
}
func (tm *Manager) SignalCommunityTokenReceived(address common.Address, txHash common.Hash, value *big.Int, t *Token, isFirst bool) {
if tm.walletFeed == nil || t == nil || t.CommunityData == nil {
return
}
if len(t.CommunityData.Name) == 0 {
_ = tm.fillCommunityData(t)
}
if len(t.CommunityData.Name) == 0 && tm.communityManager != nil {
communityData, _ := tm.communityManager.FetchCommunityMetadata(t.CommunityData.ID)
if communityData != nil {
t.CommunityData.Name = communityData.CommunityName
t.CommunityData.Color = communityData.CommunityColor
t.CommunityData.Image = tm.communityManager.GetCommunityImageURL(t.CommunityData.ID)
}
}
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()
t.Image = tm.mediaServer.MakeCommunityTokenImagesURL(t.CommunityData.ID, t.ChainID, t.Symbol)
receivedToken := ReceivedToken{
Token: *t,
Amount: floatAmount,
TxHash: txHash,
IsFirst: isFirst,
}
encodedMessage, err := json.Marshal(receivedToken)
if err != nil {
return
}
tm.walletFeed.Send(walletevent.Event{
Type: EventCommunityTokenReceived,
ChainID: t.ChainID,
Accounts: []common.Address{
address,
},
Message: string(encodedMessage),
})
}
func (tm *Manager) fillCommunityData(token *Token) error {
if token == nil || token.CommunityData == nil || tm.communityManager == nil {
return nil
}
communityInfo, _, err := tm.communityManager.GetCommunityInfo(token.CommunityData.ID)
if err != nil {
return err
}
if err == nil && communityInfo != nil {
// Fetched data from cache. Cache is refreshed during every wallet token list call.
token.CommunityData.Name = communityInfo.CommunityName
token.CommunityData.Color = communityInfo.CommunityColor
token.CommunityData.Image = communityInfo.CommunityImage
}
return nil
}
func (tm *Manager) GetTokenHistoricalBalance(account common.Address, chainID uint64, symbol string, timestamp int64) (*big.Int, error) {
var balance big.Int
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))
if err == sql.ErrNoRows {
return nil, nil
} else if err != nil {
return nil, err
}
return &balance, nil
}
func (tm *Manager) GetPreviouslyOwnedTokens() (map[common.Address][]*Token, error) {
tokenMap := make(map[common.Address][]*Token)
rows, err := tm.db.Query("SELECT user_address, token_name, token_symbol, token_address, token_decimals, chain_id FROM token_balances")
if err != nil {
return nil, err
}
defer rows.Close()
for rows.Next() {
token := &Token{}
var addressStr, tokenAddressStr string
err := rows.Scan(&addressStr, &token.Name, &token.Symbol, &tokenAddressStr, &token.Decimals, &token.ChainID)
if err != nil {
return nil, err
}
address := common.HexToAddress(addressStr)
if (address == common.Address{}) {
continue
}
token.Address = common.HexToAddress(tokenAddressStr)
if (token.Address == common.Address{}) {
continue
}
if _, ok := tokenMap[address]; !ok {
tokenMap[address] = make([]*Token, 0)
}
tokenMap[address] = append(tokenMap[address], token)
}
return tokenMap, nil
}
func (tm *Manager) removeTokenBalances(account common.Address) error {
_, err := tm.db.Exec("DELETE FROM token_balances WHERE user_address = ?", account.String())
return err
}
func (tm *Manager) onAccountsChange(changedAddresses []common.Address, eventType accountsevent.EventType, currentAddresses []common.Address) {
if eventType == accountsevent.EventTypeRemoved {
for _, account := range changedAddresses {
err := tm.removeTokenBalances(account)
if err != nil {
log.Error("token.Manager: can't remove token balances", "error", err)
}
}
}
}