status-go/services/wallet/token.go

446 lines
11 KiB
Go

package wallet
import (
"context"
"database/sql"
"errors"
"math/big"
"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/log"
"github.com/status-im/status-go/contracts"
"github.com/status-im/status-go/contracts/ierc20"
"github.com/status-im/status-go/rpc"
"github.com/status-im/status-go/rpc/network"
"github.com/status-im/status-go/services/wallet/async"
"github.com/status-im/status-go/services/wallet/chain"
)
var requestTimeout = 20 * time.Second
var nativeChainAddress = common.HexToAddress("0x")
type Token struct {
Address common.Address `json:"address"`
Name string `json:"name"`
Symbol string `json:"symbol"`
Color string `json:"color"`
// 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"`
}
type TokenManager struct {
db *sql.DB
RPCClient *rpc.Client
networkManager *network.Manager
}
func NewTokenManager(
db *sql.DB,
RPCClient *rpc.Client,
networkManager *network.Manager,
) *TokenManager {
tokenManager := &TokenManager{db, RPCClient, networkManager}
// Check the networks' custom tokens to see if we must update the tokenStore
networks := networkManager.GetConfiguredNetworks()
for _, network := range networks {
if len(network.TokenOverrides) == 0 {
continue
}
for _, overrideToken := range network.TokenOverrides {
tokensMap, ok := tokenStore[network.ChainID]
if !ok {
continue
}
for _, token := range tokensMap {
if token.Symbol == overrideToken.Symbol {
token.Address = overrideToken.Address
}
}
}
}
return tokenManager
}
func (tm *TokenManager) findSNT(chainID uint64) *Token {
tokensMap, ok := tokenStore[chainID]
if !ok {
return nil
}
for _, token := range tokensMap {
if token.Symbol == "SNT" || token.Symbol == "STT" {
return token
}
}
return nil
}
func (tm *TokenManager) getTokens(chainID uint64) ([]*Token, error) {
tokensMap, ok := tokenStore[chainID]
if !ok {
return nil, errors.New("no tokens for this network")
}
res := make([]*Token, 0, len(tokensMap))
for _, token := range tokensMap {
res = append(res, token)
}
return res, nil
}
func (tm *TokenManager) discoverToken(ctx context.Context, chainID uint64, address common.Address) (*Token, error) {
backend, err := tm.RPCClient.EthClient(chainID)
if err != nil {
return nil, err
}
caller, err := ierc20.NewIERC20Caller(address, backend)
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),
}, nil
}
func (tm *TokenManager) getCustoms() ([]*Token, error) {
rows, err := tm.db.Query("SELECT address, name, symbol, decimals, color, network_id FROM tokens")
if err != nil {
return nil, err
}
defer rows.Close()
var rst []*Token
for rows.Next() {
token := &Token{}
err := rows.Scan(&token.Address, &token.Name, &token.Symbol, &token.Decimals, &token.Color, &token.ChainID)
if err != nil {
return nil, err
}
rst = append(rst, token)
}
return rst, nil
}
func (tm *TokenManager) getCustomsByChainID(chainID uint64) ([]*Token, error) {
rows, err := tm.db.Query("SELECT address, name, symbol, decimals, color, network_id FROM tokens where network_id=?", chainID)
if err != nil {
return nil, err
}
defer rows.Close()
var rst []*Token
for rows.Next() {
token := &Token{}
err := rows.Scan(&token.Address, &token.Name, &token.Symbol, &token.Decimals, &token.Color, &token.ChainID)
if err != nil {
return nil, err
}
rst = append(rst, token)
}
return rst, nil
}
func (tm *TokenManager) isTokenVisible(chainID uint64, address common.Address) (bool, error) {
rows, err := tm.db.Query("SELECT chain_id, address FROM visible_tokens WHERE chain_id = ? AND address = ?", chainID, address)
if err != nil {
return false, err
}
defer rows.Close()
return rows.Next(), nil
}
func (tm *TokenManager) toggle(chainID uint64, address common.Address) error {
isVisible, err := tm.isTokenVisible(chainID, address)
if err != nil {
return err
}
if isVisible {
_, err = tm.db.Exec(`DELETE FROM visible_tokens WHERE address = ? and chain_id = ?`, address, chainID)
return err
}
insert, err := tm.db.Prepare("INSERT OR REPLACE INTO visible_tokens (chain_id, address) VALUES (?, ?)")
if err != nil {
return err
}
defer insert.Close()
_, err = insert.Exec(chainID, address)
return err
}
func (tm *TokenManager) getVisible(chainIDs []uint64) (map[uint64][]*Token, error) {
customTokens, err := tm.getCustoms()
if err != nil {
return nil, err
}
rst := make(map[uint64][]*Token)
for _, chainID := range chainIDs {
network := tm.networkManager.Find(chainID)
if network == nil {
continue
}
rst[chainID] = make([]*Token, 0)
rst[chainID] = append(rst[chainID], &Token{
Address: common.HexToAddress("0x"),
Name: network.NativeCurrencyName,
Symbol: network.NativeCurrencySymbol,
Decimals: uint(network.NativeCurrencyDecimals),
ChainID: chainID,
})
}
rows, err := tm.db.Query("SELECT chain_id, address FROM visible_tokens")
if err != nil {
return nil, err
}
defer rows.Close()
for rows.Next() {
address := common.HexToAddress("0x")
chainID := uint64(0)
err := rows.Scan(&chainID, &address)
if err != nil {
return nil, err
}
found := false
for _, token := range tokenStore[chainID] {
if token.Address == address {
rst[chainID] = append(rst[chainID], token)
found = true
break
}
}
if found {
continue
}
for _, token := range customTokens {
if token.Address == address {
rst[chainID] = append(rst[chainID], token)
break
}
}
}
for _, chainID := range chainIDs {
if len(rst[chainID]) == 1 {
token := tm.findSNT(chainID)
if token != nil {
rst[chainID] = append(rst[chainID], token)
}
}
}
return rst, nil
}
func (tm *TokenManager) upsertCustom(token Token) error {
insert, err := tm.db.Prepare("INSERT OR REPLACE INTO TOKENS (network_id, address, name, symbol, decimals, color) VALUES (?, ?, ?, ?, ?, ?)")
if err != nil {
return err
}
_, err = insert.Exec(token.ChainID, token.Address, token.Name, token.Symbol, token.Decimals, token.Color)
return err
}
func (tm *TokenManager) 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 *TokenManager) getTokenBalance(ctx context.Context, client *chain.Client, account common.Address, token common.Address) (*big.Int, error) {
caller, err := ierc20.NewIERC20Caller(token, client)
if err != nil {
return nil, err
}
return caller.BalanceOf(&bind.CallOpts{
Context: ctx,
}, account)
}
func (tm *TokenManager) getChainBalance(ctx context.Context, client *chain.Client, account common.Address) (*big.Int, error) {
return client.BalanceAt(ctx, account, nil)
}
func (tm *TokenManager) getBalance(ctx context.Context, client *chain.Client, account common.Address, token common.Address) (*big.Int, error) {
if token == nativeChainAddress {
return tm.getChainBalance(ctx, client, account)
}
return tm.getTokenBalance(ctx, client, account, token)
}
func (tm *TokenManager) getBalances(parent context.Context, clients []*chain.Client, accounts, tokens []common.Address) (map[common.Address]map[common.Address]*hexutil.Big, error) {
var (
group = async.NewAtomicGroup(parent)
mu sync.Mutex
response = map[common.Address]map[common.Address]*hexutil.Big{}
)
updateBalance := func(account common.Address, token common.Address, balance *big.Int) {
mu.Lock()
if _, ok := response[account]; !ok {
response[account] = map[common.Address]*hexutil.Big{}
}
if _, ok := response[account][token]; !ok {
zeroHex := hexutil.Big(*big.NewInt(0))
response[account][token] = &zeroHex
}
sum := big.NewInt(0).Add(response[account][token].ToInt(), balance)
sumHex := hexutil.Big(*sum)
response[account][token] = &sumHex
mu.Unlock()
}
contractMaker := contracts.ContractMaker{RPCClient: tm.RPCClient}
for clientIdx := range clients {
client := clients[clientIdx]
ethScanContract, err := contractMaker.NewEthScan(client.ChainID)
if err == nil {
fetchChainBalance := false
var tokenChunks [][]common.Address
chunkSize := 100
for i := 0; i < len(tokens); i += chunkSize {
end := i + chunkSize
if end > len(tokens) {
end = len(tokens)
}
tokenChunks = append(tokenChunks, tokens[i:end])
}
for _, token := range tokens {
if token == nativeChainAddress {
fetchChainBalance = true
}
}
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,
}, accounts)
if err != nil {
log.Error("can't fetch chain balance", err)
return nil
}
for idx, account := range accounts {
balance := new(big.Int)
balance.SetBytes(res[idx].Data)
updateBalance(account, common.HexToAddress("0x"), balance)
}
return nil
})
}
for accountIdx := range accounts {
account := accounts[accountIdx]
for idx := range tokenChunks {
chunk := tokenChunks[idx]
group.Add(func(parent context.Context) error {
ctx, cancel := context.WithTimeout(parent, requestTimeout)
defer cancel()
res, err := ethScanContract.TokensBalance(&bind.CallOpts{
Context: ctx,
}, account, chunk)
if err != nil {
log.Error("can't fetch erc20 token balance", "account", account, "error", err)
return nil
}
for idx, token := range chunk {
if !res[idx].Success {
continue
}
balance := new(big.Int)
balance.SetBytes(res[idx].Data)
updateBalance(account, token, balance)
}
return nil
})
}
}
} else {
for tokenIdx := range tokens {
for accountIdx := range accounts {
// Below, we set account, token and client from idx on purpose to avoid override
account := accounts[accountIdx]
token := tokens[tokenIdx]
client := clients[clientIdx]
group.Add(func(parent context.Context) error {
ctx, cancel := context.WithTimeout(parent, requestTimeout)
defer cancel()
balance, err := tm.getBalance(ctx, client, account, token)
if err != nil {
log.Error("can't fetch erc20 token balance", "account", account, "token", token, "error", err)
return nil
}
updateBalance(account, token, balance)
return nil
})
}
}
}
}
select {
case <-group.WaitAsync():
case <-parent.Done():
return nil, parent.Err()
}
return response, group.Error()
}