feat: add wallet token endpoint

This commit is contained in:
Anthony Laibe 2022-10-27 09:38:05 +02:00 committed by Anthony Laibe
parent f3b1448efa
commit eff02a79a9
4 changed files with 358 additions and 180 deletions

View File

@ -35,8 +35,8 @@ func (api *API) StartWallet(ctx context.Context, chainIDs []uint64) error {
return api.reader.Start(ctx, chainIDs)
}
func (api *API) GetWallet(ctx context.Context, chainIDs []uint64) (*Wallet, error) {
return api.reader.GetWallet(ctx, chainIDs)
func (api *API) GetWalletToken(ctx context.Context) (map[common.Address][]Token, error) {
return api.reader.GetWalletToken(ctx)
}
type DerivedAddress struct {

View File

@ -78,31 +78,49 @@ func getRealSymbol(symbol string) string {
return strings.ToUpper(symbol)
}
func chunkSymbols(symbols []string) [][]string {
var chunks [][]string
chunkSize := 20
for i := 0; i < len(symbols); i += chunkSize {
end := i + chunkSize
if end > len(symbols) {
end = len(symbols)
}
chunks = append(chunks, symbols[i:end])
}
return chunks
}
func fetchCryptoComparePrices(symbols []string, currency string) (map[string]float64, error) {
realSymbols := renameSymbols(symbols)
httpClient := http.Client{Timeout: time.Minute}
url := fmt.Sprintf("%s/data/pricemulti?fsyms=%s&tsyms=%s&extraParams=Status.im", cryptocompareURL, strings.Join(realSymbols, ","), currency)
resp, err := httpClient.Get(url)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
prices := make(map[string]map[string]float64)
err = json.Unmarshal(body, &prices)
if err != nil {
return nil, err
}
chunks := chunkSymbols(symbols)
result := make(map[string]float64)
for _, symbol := range symbols {
result[symbol] = prices[getRealSymbol(symbol)][strings.ToUpper(currency)]
for _, smbls := range chunks {
realSymbols := renameSymbols(smbls)
httpClient := http.Client{Timeout: time.Minute}
url := fmt.Sprintf("%s/data/pricemulti?fsyms=%s&tsyms=%s&extraParams=Status.im", cryptocompareURL, strings.Join(realSymbols, ","), currency)
resp, err := httpClient.Get(url)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
prices := make(map[string]map[string]float64)
err = json.Unmarshal(body, &prices)
if err != nil {
return nil, err
}
for _, symbol := range smbls {
result[symbol] = prices[getRealSymbol(symbol)][strings.ToUpper(currency)]
}
}
return result, nil
}

View File

@ -2,14 +2,15 @@ package wallet
import (
"context"
"math"
"math/big"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/status-im/status-go/multiaccounts/accounts"
"github.com/status-im/status-go/services/wallet/async"
"github.com/status-im/status-go/services/wallet/chain"
"github.com/status-im/status-go/services/wallet/token"
"github.com/status-im/status-go/services/wallet/transfer"
)
func NewReader(s *Service) *Reader {
@ -20,32 +21,29 @@ type Reader struct {
s *Service
}
type ReaderToken struct {
Token *token.Token `json:"token"`
OraclePrice float64 `json:"oraclePrice"`
CryptoBalance *hexutil.Big `json:"cryptoBalance"`
FiatBalance *big.Float `json:"fiatBalance"`
type ChainBalance struct {
Balance *big.Float `json:"balance"`
Address common.Address `json:"address"`
ChainID uint64 `json:"chainId"`
}
type ReaderAccount struct {
Account *accounts.Account `json:"account"`
Collections map[uint64][]OpenseaCollection `json:"collections"`
Tokens map[uint64][]ReaderToken `json:"tokens"`
Transactions map[uint64][]transfer.View `json:"transactions"`
FiatBalance *big.Float `json:"fiatBalance"`
}
type Wallet struct {
Accounts []ReaderAccount `json:"accounts"`
OnRamp []CryptoOnRamp `json:"onRamp"`
SavedAddresses map[uint64][]SavedAddress `json:"savedAddresses"`
Tokens map[uint64][]*token.Token `json:"tokens"`
CustomTokens []*token.Token `json:"customTokens"`
PendingTransactions map[uint64][]*PendingTransaction `json:"pendingTransactions"`
FiatBalance *big.Float `json:"fiatBalance"`
Currency string `json:"currency"`
type Token struct {
Name string `json:"name"`
Symbol string `json:"symbol"`
Color string `json:"color"`
Decimals uint `json:"decimals"`
BalancesPerChain map[uint64]ChainBalance `json:"balancesPerChain"`
Description string `json:"description"`
AssetWebsiteURL string `json:"assetWebsiteUrl"`
BuiltOn string `json:"builtOn"`
MarketCap string `json:"marketCap"`
HighDay string `json:"highDay"`
LowDay string `json:"lowDay"`
ChangePctHour string `json:"changePctHour"`
ChangePctDay string `json:"changePctDay"`
ChangePct24hour string `json:"changePct24hour"`
Change24hour string `json:"change24hour"`
CurrencyPrice float64 `json:"currencyPrice"`
}
func getAddresses(accounts []*accounts.Account) []common.Address {
@ -56,52 +54,39 @@ func getAddresses(accounts []*accounts.Account) []common.Address {
return addresses
}
func (r *Reader) buildReaderAccount(
ctx context.Context,
chainIDs []uint64,
account *accounts.Account,
visibleTokens map[uint64][]*token.Token,
prices map[string]float64,
balances map[common.Address]*hexutil.Big,
) (ReaderAccount, error) {
limit := int64(20)
toBlock := big.NewInt(0)
func getTokenBySymbols(tokens []*token.Token) map[string][]*token.Token {
res := make(map[string][]*token.Token)
collections := make(map[uint64][]OpenseaCollection)
tokens := make(map[uint64][]ReaderToken)
transactions := make(map[uint64][]transfer.View)
accountFiatBalance := big.NewFloat(0)
for _, chainID := range chainIDs {
client, err := newOpenseaClient(chainID, r.s.openseaAPIKey)
if err == nil {
c, _ := client.fetchAllCollectionsByOwner(common.Address(account.Address))
collections[chainID] = c
for _, t := range tokens {
if _, ok := res[t.Symbol]; !ok {
res[t.Symbol] = make([]*token.Token, 0)
}
for _, token := range visibleTokens[chainID] {
oraclePrice := prices[token.Symbol]
cryptoBalance := balances[token.Address].ToInt()
fiatBalance := big.NewFloat(0).Mul(big.NewFloat(oraclePrice), new(big.Float).SetInt(cryptoBalance))
tokens[chainID] = append(tokens[chainID], ReaderToken{
Token: token,
OraclePrice: oraclePrice,
CryptoBalance: balances[token.Address],
FiatBalance: fiatBalance,
})
accountFiatBalance = accountFiatBalance.Add(accountFiatBalance, fiatBalance)
}
t, err := r.s.transferController.GetTransfersByAddress(ctx, chainID, common.Address(account.Address), toBlock, limit, false)
if err == nil {
transactions[chainID] = t
}
res[t.Symbol] = append(res[t.Symbol], t)
}
return ReaderAccount{
Account: account,
Collections: collections,
Tokens: tokens,
Transactions: transactions,
FiatBalance: accountFiatBalance,
}, nil
return res
}
func getTokenSymbols(tokens []*token.Token) []string {
tokensBySymbols := getTokenBySymbols(tokens)
res := make([]string, 0)
for symbol := range tokensBySymbols {
res = append(res, symbol)
}
return res
}
func getTokenAddresses(tokens []*token.Token) []common.Address {
set := make(map[common.Address]bool)
for _, token := range tokens {
set[token.Address] = true
}
res := make([]common.Address, 0)
for address := range set {
res = append(res, address)
}
return res
}
func (r *Reader) Start(ctx context.Context, chainIDs []uint64) error {
@ -113,108 +98,135 @@ func (r *Reader) Start(ctx context.Context, chainIDs []uint64) error {
return r.s.transferController.CheckRecentHistory(chainIDs, getAddresses(accounts))
}
func (r *Reader) GetWallet(ctx context.Context, chainIDs []uint64) (*Wallet, error) {
func (r *Reader) GetWalletToken(ctx context.Context) (map[common.Address][]Token, error) {
networks, err := r.s.rpcClient.NetworkManager.Get(false)
if err != nil {
return nil, err
}
chainIDs := make([]uint64, 0)
for _, network := range networks {
chainIDs = append(chainIDs, network.ChainID)
}
currency, err := r.s.accountsDB.GetCurrency()
if err != nil {
return nil, err
}
tokensMap := make(map[uint64][]*token.Token)
for _, chainID := range chainIDs {
tokens, err := r.s.tokenManager.GetTokens(chainID)
if err != nil {
return nil, err
}
tokensMap[chainID] = tokens
}
customTokens, err := r.s.tokenManager.GetCustoms()
allTokens, err := r.s.tokenManager.GetAllTokens()
if err != nil {
return nil, err
}
visibleTokens, err := r.s.tokenManager.GetVisible(chainIDs)
if err != nil {
return nil, err
for _, network := range networks {
allTokens = append(allTokens, r.s.tokenManager.ToToken(network))
}
tokenAddresses := make([]common.Address, 0)
tokenSymbols := make([]string, 0)
for _, tokens := range visibleTokens {
for _, token := range tokens {
tokenAddresses = append(tokenAddresses, token.Address)
tokenSymbols = append(tokenSymbols, token.Symbol)
}
}
tokenSymbols := getTokenSymbols(allTokens)
tokenAddresses := getTokenAddresses(allTokens)
accounts, err := r.s.accountsDB.GetAccounts()
if err != nil {
return nil, err
}
prices, err := fetchCryptoComparePrices(tokenSymbols, currency)
if err != nil {
return nil, err
}
var (
group = async.NewAtomicGroup(ctx)
prices = map[string]float64{}
tokenDetails = map[string]Coin{}
tokenMarketValues = map[string]MarketCoinValues{}
balances = map[uint64]map[common.Address]map[common.Address]*hexutil.Big{}
)
clients, err := chain.NewClients(r.s.rpcClient, chainIDs)
if err != nil {
return nil, err
}
balances, err := r.s.tokenManager.GetBalances(ctx, clients, getAddresses(accounts), tokenAddresses)
if err != nil {
return nil, err
}
readerAccounts := make([]ReaderAccount, len(accounts))
walletFiatBalance := big.NewFloat(0)
for i, account := range accounts {
readerAccount, err := r.buildReaderAccount(
ctx,
chainIDs,
account,
visibleTokens,
prices,
balances[common.Address(account.Address)],
)
group.Add(func(parent context.Context) error {
prices, err = fetchCryptoComparePrices(tokenSymbols, currency)
if err != nil {
return nil, err
return err
}
walletFiatBalance = walletFiatBalance.Add(walletFiatBalance, readerAccount.FiatBalance)
readerAccounts[i] = readerAccount
}
return nil
})
savedAddressesMap := make(map[uint64][]SavedAddress)
for _, chainID := range chainIDs {
savedAddresses, err := r.s.savedAddressesManager.GetSavedAddressesForChainID(chainID)
group.Add(func(parent context.Context) error {
tokenDetails, err = fetchCryptoCompareTokenDetails(tokenSymbols)
if err != nil {
return nil, err
return err
}
savedAddressesMap[chainID] = savedAddresses
}
return nil
})
onRamp, err := r.s.cryptoOnRampManager.Get()
group.Add(func(parent context.Context) error {
tokenMarketValues, err = fetchTokenMarketValues(tokenSymbols, currency)
if err != nil {
return err
}
return nil
})
group.Add(func(parent context.Context) error {
clients, err := chain.NewClients(r.s.rpcClient, chainIDs)
if err != nil {
return err
}
balances, err = r.s.tokenManager.GetBalancesByChain(ctx, clients, getAddresses(accounts), tokenAddresses)
if err != nil {
return err
}
return nil
})
select {
case <-group.WaitAsync():
case <-ctx.Done():
return nil, ctx.Err()
}
err = group.Error()
if err != nil {
return nil, err
}
result := make(map[common.Address][]Token)
for _, account := range accounts {
commonAddress := common.Address(account.Address)
for symbol, tokens := range getTokenBySymbols(allTokens) {
balancesPerChain := make(map[uint64]ChainBalance)
decimals := tokens[0].Decimals
for _, token := range tokens {
hexBalance := balances[token.ChainID][commonAddress][token.Address]
balance := big.NewFloat(0.0)
if hexBalance != nil {
balance = new(big.Float).Quo(
new(big.Float).SetInt(hexBalance.ToInt()),
big.NewFloat(math.Pow(10, float64(decimals))),
)
}
balancesPerChain[token.ChainID] = ChainBalance{
Balance: balance,
Address: token.Address,
ChainID: token.ChainID,
}
}
pendingTransactions := make(map[uint64][]*PendingTransaction)
for _, chainID := range chainIDs {
pendingTx, err := r.s.transactionManager.getAllPendings([]uint64{chainID})
if err != nil {
return nil, err
walletToken := Token{
Name: tokens[0].Name,
Color: tokens[0].Color,
Symbol: symbol,
BalancesPerChain: balancesPerChain,
Decimals: decimals,
Description: tokenDetails[symbol].Description,
AssetWebsiteURL: tokenDetails[symbol].AssetWebsiteURL,
BuiltOn: tokenDetails[symbol].BuiltOn,
MarketCap: tokenMarketValues[symbol].MKTCAP,
HighDay: tokenMarketValues[symbol].HIGHDAY,
LowDay: tokenMarketValues[symbol].LOWDAY,
ChangePctHour: tokenMarketValues[symbol].CHANGEPCTHOUR,
ChangePctDay: tokenMarketValues[symbol].CHANGEPCTDAY,
ChangePct24hour: tokenMarketValues[symbol].CHANGEPCT24HOUR,
Change24hour: tokenMarketValues[symbol].CHANGE24HOUR,
CurrencyPrice: prices[symbol],
}
result[commonAddress] = append(result[commonAddress], walletToken)
}
pendingTransactions[chainID] = pendingTx
}
return &Wallet{
Accounts: readerAccounts,
OnRamp: onRamp,
SavedAddresses: savedAddressesMap,
Tokens: tokensMap,
CustomTokens: customTokens,
PendingTransactions: pendingTransactions,
Currency: currency,
FiatBalance: walletFiatBalance,
}, nil
return result, nil
}

View File

@ -77,12 +77,7 @@ func NewTokenManager(
func (tm *Manager) FindToken(network *params.Network, tokenSymbol string) *Token {
if tokenSymbol == network.NativeCurrencySymbol {
return &Token{
Address: common.HexToAddress("0x"),
Symbol: network.NativeCurrencySymbol,
Decimals: uint(network.NativeCurrencyDecimals),
Name: network.NativeCurrencyName,
}
return tm.ToToken(network)
}
tokens, err := tm.GetTokens(network.ChainID)
@ -117,6 +112,25 @@ func (tm *Manager) FindSNT(chainID uint64) *Token {
return nil
}
func (tm *Manager) GetAllTokens() ([]*Token, error) {
result := make([]*Token, 0)
for _, tokens := range tokenStore {
for _, token := range tokens {
result = append(result, token)
}
}
tokens, err := tm.GetCustoms()
if err != nil {
return nil, err
}
for _, token := range tokens {
result = append(result, token)
}
return result, nil
}
func (tm *Manager) GetTokens(chainID uint64) ([]*Token, error) {
tokensMap, ok := tokenStore[chainID]
if !ok {
@ -244,6 +258,16 @@ func (tm *Manager) Toggle(chainID uint64, address common.Address) error {
return err
}
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,
}
}
func (tm *Manager) GetVisible(chainIDs []uint64) (map[uint64][]*Token, error) {
customTokens, err := tm.GetCustoms()
if err != nil {
@ -258,13 +282,7 @@ func (tm *Manager) GetVisible(chainIDs []uint64) (map[uint64][]*Token, error) {
}
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,
})
rst[chainID] = append(rst[chainID], tm.ToToken(network))
}
rows, err := tm.db.Query("SELECT chain_id, address FROM visible_tokens")
if err != nil {
@ -475,3 +493,133 @@ func (tm *Manager) GetBalances(parent context.Context, clients []*chain.Client,
}
return response, group.Error()
}
func (tm *Manager) GetBalancesByChain(parent context.Context, clients []*chain.Client, accounts, tokens []common.Address) (map[uint64]map[common.Address]map[common.Address]*hexutil.Big, error) {
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()
}
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(client.ChainID, 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(client.ChainID, 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(client.ChainID, account, token, balance)
return nil
})
}
}
}
}
select {
case <-group.WaitAsync():
case <-parent.Done():
return nil, parent.Err()
}
return response, group.Error()
}