feat: get wallet api (#2619)

This commit is contained in:
Anthony Laibe 2022-05-10 09:48:05 +02:00 committed by GitHub
parent 017ae1e205
commit e199ddbe9d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 282 additions and 21 deletions

View File

@ -510,6 +510,10 @@ func (db *Database) GetPreferredUsername() (string, error) {
return db.makeSelectString(PreferredName) return db.makeSelectString(PreferredName)
} }
func (db *Database) GetCurrency() (string, error) {
return db.makeSelectString(Currency)
}
func (db *Database) GetInstalledStickerPacks() (rst *json.RawMessage, err error) { func (db *Database) GetInstalledStickerPacks() (rst *json.RawMessage, err error) {
err = db.makeSelectRow(StickersPacksInstalled).Scan(&rst) err = db.makeSelectRow(StickersPacksInstalled).Scan(&rst)
return return

View File

@ -124,7 +124,7 @@ func (b *StatusNode) initServices(config *params.NodeConfig) error {
if len(openseaKey) == 0 { if len(openseaKey) == 0 {
openseaKey = OpenseaKeyFromEnv openseaKey = OpenseaKeyFromEnv
} }
walletService := b.walletService(accountsFeed, openseaKey) walletService := b.walletService(accDB, accountsFeed, openseaKey)
services = append(services, walletService) services = append(services, walletService)
} }
@ -441,9 +441,9 @@ func (b *StatusNode) appmetricsService() common.StatusService {
return b.appMetricsSrvc return b.appMetricsSrvc
} }
func (b *StatusNode) walletService(accountsFeed *event.Feed, openseaAPIKey string) common.StatusService { func (b *StatusNode) walletService(accountsDB *accounts.Database, accountsFeed *event.Feed, openseaAPIKey string) common.StatusService {
if b.walletSrvc == nil { if b.walletSrvc == nil {
b.walletSrvc = wallet.NewService(b.appDB, b.rpcClient, accountsFeed, openseaAPIKey) b.walletSrvc = wallet.NewService(b.appDB, accountsDB, b.rpcClient, accountsFeed, openseaAPIKey)
} }
return b.walletSrvc return b.walletSrvc
} }

View File

@ -12,12 +12,22 @@ import (
) )
func NewAPI(s *Service) *API { func NewAPI(s *Service) *API {
return &API{s} r := NewReader(s)
return &API{s, r}
} }
// API is class with methods available over RPC. // API is class with methods available over RPC.
type API struct { type API struct {
s *Service s *Service
r *Reader
}
func (api *API) StartWallet(ctx context.Context, chainIDs []uint64) error {
return api.r.Start(ctx, chainIDs)
}
func (api *API) GetWallet(ctx context.Context, chainIDs []uint64) (*Wallet, error) {
return api.r.GetWallet(ctx, chainIDs)
} }
// SetInitialBlocksRange sets initial blocks range // SetInitialBlocksRange sets initial blocks range
@ -139,7 +149,7 @@ func (api *API) DeleteCustomTokenByChainID(ctx context.Context, chainID uint64,
return err return err
} }
func (api *API) GetSavedAddresses(ctx context.Context) ([]*SavedAddress, error) { func (api *API) GetSavedAddresses(ctx context.Context) ([]SavedAddress, error) {
log.Debug("call to get saved addresses") log.Debug("call to get saved addresses")
rst, err := api.s.savedAddressesManager.GetSavedAddresses(api.s.rpcClient.UpstreamChainID) rst, err := api.s.savedAddressesManager.GetSavedAddresses(api.s.rpcClient.UpstreamChainID)
log.Debug("result from database for saved addresses", "len", len(rst)) log.Debug("result from database for saved addresses", "len", len(rst))
@ -231,7 +241,7 @@ func (api *API) WatchTransactionByChainID(ctx context.Context, chainID uint64, t
return api.s.transactionManager.watch(ctx, transactionHash, chainClient) return api.s.transactionManager.watch(ctx, transactionHash, chainClient)
} }
func (api *API) GetFavourites(ctx context.Context) ([]*Favourite, error) { func (api *API) GetFavourites(ctx context.Context) ([]Favourite, error) {
log.Debug("call to get favourites") log.Debug("call to get favourites")
rst, err := api.s.favouriteManager.GetFavourites() rst, err := api.s.favouriteManager.GetFavourites()
log.Debug("result from database for favourites", "len", len(rst)) log.Debug("result from database for favourites", "len", len(rst))

View File

@ -15,16 +15,16 @@ type FavouriteManager struct {
db *sql.DB db *sql.DB
} }
func (fm *FavouriteManager) GetFavourites() ([]*Favourite, error) { func (fm *FavouriteManager) GetFavourites() ([]Favourite, error) {
rows, err := fm.db.Query(`SELECT address, name FROM favourites`) rows, err := fm.db.Query(`SELECT address, name FROM favourites`)
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer rows.Close() defer rows.Close()
var rst []*Favourite var rst []Favourite
for rows.Next() { for rows.Next() {
favourite := &Favourite{} favourite := Favourite{}
err := rows.Scan(&favourite.Address, &favourite.Name) err := rows.Scan(&favourite.Address, &favourite.Name)
if err != nil { if err != nil {
return nil, err return nil, err

226
services/wallet/reader.go Normal file
View File

@ -0,0 +1,226 @@
package wallet
import (
"context"
"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/chain"
"github.com/status-im/status-go/services/wallet/transfer"
)
func NewReader(s *Service) *Reader {
return &Reader{s}
}
type Reader struct {
s *Service
}
type ReaderToken struct {
Token *Token `json:"token"`
OraclePrice float64 `json:"oraclePrice"`
CryptoBalance *hexutil.Big `json:"cryptoBalance"`
FiatBalance *big.Float `json:"fiatBalance"`
}
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"`
Favorites []Favourite `json:"favorites"`
OnRamp []CryptoOnRamp `json:"onRamp"`
SavedAddresses map[uint64][]SavedAddress `json:"savedAddresses"`
Tokens map[uint64][]*Token `json:"tokens"`
CustomTokens []*Token `json:"customTokens"`
PendingTransactions map[uint64][]*PendingTransaction `json:"pendingTransactions"`
FiatBalance *big.Float `json:"fiatBalance"`
Currency string `json:"currency"`
}
func getAddresses(accounts []accounts.Account) []common.Address {
addresses := make([]common.Address, len(accounts))
for _, account := range accounts {
addresses = append(addresses, common.Address(account.Address))
}
return addresses
}
func (r *Reader) buildReaderAccount(
ctx context.Context,
chainIDs []uint64,
account accounts.Account,
visibleTokens map[uint64][]*Token,
prices map[string]float64,
balances map[common.Address]*hexutil.Big,
) (ReaderAccount, error) {
limit := (*hexutil.Big)(big.NewInt(20))
toBlock := (*hexutil.Big)(big.NewInt(0))
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 _, 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
}
}
return ReaderAccount{
Account: &account,
Collections: collections,
Tokens: tokens,
Transactions: transactions,
FiatBalance: accountFiatBalance,
}, nil
}
func (r *Reader) Start(ctx context.Context, chainIDs []uint64) error {
accounts, err := r.s.accountsDB.GetAccounts()
if err != nil {
return err
}
return r.s.transferController.CheckRecentHistory(chainIDs, getAddresses(accounts))
}
func (r *Reader) GetWallet(ctx context.Context, chainIDs []uint64) (*Wallet, error) {
currency, err := r.s.accountsDB.GetCurrency()
if err != nil {
return nil, err
}
tokensMap := make(map[uint64][]*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()
if err != nil {
return nil, err
}
visibleTokens, err := r.s.tokenManager.getVisible(chainIDs)
if err != nil {
return nil, err
}
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)
}
}
accounts, err := r.s.accountsDB.GetAccounts()
if err != nil {
return nil, err
}
prices, err := fetchCryptoComparePrices(tokenSymbols, currency)
if err != nil {
return nil, err
}
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)],
)
if err != nil {
return nil, err
}
walletFiatBalance = walletFiatBalance.Add(walletFiatBalance, readerAccount.FiatBalance)
readerAccounts[i] = readerAccount
}
savedAddressesMap := make(map[uint64][]SavedAddress)
for _, chainID := range chainIDs {
savedAddresses, err := r.s.savedAddressesManager.GetSavedAddresses(chainID)
if err != nil {
return nil, err
}
savedAddressesMap[chainID] = savedAddresses
}
onRamp, err := r.s.cryptoOnRampManager.Get()
if err != nil {
return nil, err
}
favorites, err := r.s.favouriteManager.GetFavourites()
if err != nil {
return nil, err
}
pendingTransactions := make(map[uint64][]*PendingTransaction)
for _, chainID := range chainIDs {
pendingTx, err := r.s.transactionManager.getAllPendings(chainID)
if err != nil {
return nil, err
}
pendingTransactions[chainID] = pendingTx
}
return &Wallet{
Accounts: readerAccounts,
Favorites: favorites,
OnRamp: onRamp,
SavedAddresses: savedAddressesMap,
Tokens: tokensMap,
CustomTokens: customTokens,
PendingTransactions: pendingTransactions,
Currency: currency,
FiatBalance: walletFiatBalance,
}, nil
}

View File

@ -18,16 +18,16 @@ type SavedAddressesManager struct {
db *sql.DB db *sql.DB
} }
func (sam *SavedAddressesManager) GetSavedAddresses(chainID uint64) ([]*SavedAddress, error) { func (sam *SavedAddressesManager) GetSavedAddresses(chainID uint64) ([]SavedAddress, error) {
rows, err := sam.db.Query("SELECT address, name, network_id FROM saved_addresses WHERE network_id = ?", chainID) rows, err := sam.db.Query("SELECT address, name, network_id FROM saved_addresses WHERE network_id = ?", chainID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer rows.Close() defer rows.Close()
var rst []*SavedAddress var rst []SavedAddress
for rows.Next() { for rows.Next() {
sa := &SavedAddress{} sa := SavedAddress{}
err := rows.Scan(&sa.Address, &sa.Name, &sa.ChainID) err := rows.Scan(&sa.Address, &sa.Name, &sa.ChainID)
if err != nil { if err != nil {
return nil, err return nil, err

View File

@ -43,7 +43,7 @@ func TestSavedAddresses(t *testing.T) {
rst, err = manager.GetSavedAddresses(777) rst, err = manager.GetSavedAddresses(777)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, 1, len(rst)) require.Equal(t, 1, len(rst))
require.Equal(t, sa, *rst[0]) require.Equal(t, sa, rst[0])
err = manager.DeleteSavedAddress(777, sa.Address) err = manager.DeleteSavedAddress(777, sa.Address)
require.NoError(t, err) require.NoError(t, err)

View File

@ -8,12 +8,13 @@ import (
"github.com/ethereum/go-ethereum/p2p" "github.com/ethereum/go-ethereum/p2p"
gethrpc "github.com/ethereum/go-ethereum/rpc" gethrpc "github.com/ethereum/go-ethereum/rpc"
"github.com/status-im/status-go/multiaccounts/accounts"
"github.com/status-im/status-go/rpc" "github.com/status-im/status-go/rpc"
"github.com/status-im/status-go/services/wallet/transfer" "github.com/status-im/status-go/services/wallet/transfer"
) )
// NewService initializes service instance. // NewService initializes service instance.
func NewService(db *sql.DB, rpcClient *rpc.Client, accountFeed *event.Feed, openseaAPIKey string) *Service { func NewService(db *sql.DB, accountsDB *accounts.Database, rpcClient *rpc.Client, accountFeed *event.Feed, openseaAPIKey string) *Service {
cryptoOnRampManager := NewCryptoOnRampManager(&CryptoOnRampOptions{ cryptoOnRampManager := NewCryptoOnRampManager(&CryptoOnRampOptions{
dataSourceType: DataSourceStatic, dataSourceType: DataSourceStatic,
}) })
@ -24,6 +25,8 @@ func NewService(db *sql.DB, rpcClient *rpc.Client, accountFeed *event.Feed, open
transferController := transfer.NewTransferController(db, rpcClient, accountFeed) transferController := transfer.NewTransferController(db, rpcClient, accountFeed)
return &Service{ return &Service{
db: db,
accountsDB: accountsDB,
rpcClient: rpcClient, rpcClient: rpcClient,
favouriteManager: favouriteManager, favouriteManager: favouriteManager,
tokenManager: tokenManager, tokenManager: tokenManager,
@ -38,6 +41,8 @@ func NewService(db *sql.DB, rpcClient *rpc.Client, accountFeed *event.Feed, open
// Service is a wallet service. // Service is a wallet service.
type Service struct { type Service struct {
db *sql.DB
accountsDB *accounts.Database
rpcClient *rpc.Client rpcClient *rpc.Client
savedAddressesManager *SavedAddressesManager savedAddressesManager *SavedAddressesManager
tokenManager *TokenManager tokenManager *TokenManager

View File

@ -242,6 +242,21 @@ func (tm *TokenManager) deleteCustom(chainID uint64, address common.Address) err
return err 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) getBalances(parent context.Context, clients []*chain.Client, accounts, tokens []common.Address) (map[common.Address]map[common.Address]*hexutil.Big, error) { func (tm *TokenManager) getBalances(parent context.Context, clients []*chain.Client, accounts, tokens []common.Address) (map[common.Address]map[common.Address]*hexutil.Big, error) {
var ( var (
group = async.NewAtomicGroup(parent) group = async.NewAtomicGroup(parent)
@ -250,10 +265,6 @@ func (tm *TokenManager) getBalances(parent context.Context, clients []*chain.Cli
) )
for _, client := range clients { for _, client := range clients {
for tokenIdx := range tokens { for tokenIdx := range tokens {
caller, err := ierc20.NewIERC20Caller(tokens[tokenIdx], client)
if err != nil {
return nil, err
}
for accountIdx := range accounts { for accountIdx := range accounts {
// Below, we set account and token from idx on purpose to avoid override // Below, we set account and token from idx on purpose to avoid override
account := accounts[accountIdx] account := accounts[accountIdx]
@ -261,9 +272,14 @@ func (tm *TokenManager) getBalances(parent context.Context, clients []*chain.Cli
group.Add(func(parent context.Context) error { group.Add(func(parent context.Context) error {
ctx, cancel := context.WithTimeout(parent, requestTimeout) ctx, cancel := context.WithTimeout(parent, requestTimeout)
defer cancel() defer cancel()
balance, err := caller.BalanceOf(&bind.CallOpts{ var balance *big.Int
Context: ctx, var err error
}, account) if token == common.HexToAddress("0x") {
balance, err = tm.getChainBalance(ctx, client, account)
} else {
balance, err = tm.getTokenBalance(ctx, client, account, token)
}
// We don't want to return an error here and prevent // We don't want to return an error here and prevent
// the rest from completing // the rest from completing
if err != nil { if err != nil {