trustwallet-assets/internal/processor/updaters_manual.go
Daniel e7f9efcd2c
Add tokenlist validation (#17111)
* Add tokenlist validation

* Fix according to review
2022-01-07 14:45:16 +03:00

531 lines
14 KiB
Go

package processor
import (
"encoding/json"
"fmt"
"strconv"
"strings"
log "github.com/sirupsen/logrus"
fileLib "github.com/trustwallet/assets-go-libs/file"
"github.com/trustwallet/assets-go-libs/http"
"github.com/trustwallet/assets-go-libs/path"
"github.com/trustwallet/assets/internal/config"
"github.com/trustwallet/go-libs/client/api/backend"
"github.com/trustwallet/go-primitives/address"
"github.com/trustwallet/go-primitives/coin"
"github.com/trustwallet/go-primitives/types"
)
var (
UniswapTradingPairsQuery = map[string]string{
"operationName": "pairs",
"query": `
query pairs {
pairs(first: 800, orderBy: reserveUSD, orderDirection: desc) {
id reserveUSD trackedReserveETH volumeUSD txCount untrackedVolumeUSD __typename
token0 {
id symbol name decimals __typename
}
token1 {
id symbol name decimals __typename
}
}
}
`,
}
PancakeSwapTradingPairsQuery = map[string]string{
"operationName": "pairs",
"query": `
query pairs {
pairs(first: 300, orderBy: reserveUSD, orderDirection: desc) {
id reserveUSD volumeUSD txCount __typename
token0 {
id symbol name decimals __typename
}
token1 {
id symbol name decimals __typename
}
}
}
`,
}
)
// nolint:dupl
func (s *Service) UpdateEthereumTokenlist() error {
log.WithFields(log.Fields{
"limit_liquidity": config.Default.TradingPairSettings.Uniswap.MinLiquidity,
"volume": config.Default.TradingPairSettings.Uniswap.MinVol24,
"tx_count": config.Default.TradingPairSettings.Uniswap.MinTxCount24,
}).Debug("Retrieving pairs from Uniswap")
forceInclude := strings.Split(config.Default.TradingPairSettings.Uniswap.ForceIncludeList, ",")
forceExclude := strings.Split(config.Default.TradingPairSettings.Uniswap.ForceExcludeList, ",")
primaryTokens := strings.Split(config.Default.TradingPairSettings.Uniswap.PrimaryTokens, ",")
tradingPairs, err := retrievePairs(config.Default.TradingPairSettings.Uniswap.URL, UniswapTradingPairsQuery,
config.Default.TradingPairSettings.Uniswap.MinLiquidity, config.Default.TradingPairSettings.Uniswap.MinVol24,
config.Default.TradingPairSettings.Uniswap.MinTxCount24, forceInclude, primaryTokens)
if err != nil {
return err
}
pairs := make([][]TokenItem, 0)
chain := coin.Coins[coin.ETHEREUM]
for _, tradingPair := range tradingPairs {
tokenItem0, err := getTokenInfoFromSubgraphToken(chain, tradingPair.Token0)
if err != nil {
return err
}
tokenItem1, err := getTokenInfoFromSubgraphToken(chain, tradingPair.Token1)
if err != nil {
return err
}
if !isTokenPrimary(tradingPair.Token0, primaryTokens) {
tokenItem0, tokenItem1 = tokenItem1, tokenItem0
}
pairs = append(pairs, []TokenItem{*tokenItem0, *tokenItem1})
}
return rebuildTokenList(chain, pairs, forceExclude)
}
// nolint:dupl
func (s *Service) UpdateSmartchainTokenlist() error {
log.WithFields(log.Fields{
"limit_liquidity": config.Default.TradingPairSettings.Pancakeswap.MinLiquidity,
"volume": config.Default.TradingPairSettings.Pancakeswap.MinVol24,
"tx_count": config.Default.TradingPairSettings.Pancakeswap.MinTxCount24,
}).Debug("Retrieving pairs from PancakeSwap")
forceInclude := strings.Split(config.Default.TradingPairSettings.Pancakeswap.ForceIncludeList, ",")
forceExclude := strings.Split(config.Default.TradingPairSettings.Pancakeswap.ForceExcludeList, ",")
primaryTokens := strings.Split(config.Default.TradingPairSettings.Pancakeswap.PrimaryTokens, ",")
tradingPairs, err := retrievePairs(config.Default.TradingPairSettings.Pancakeswap.URL,
PancakeSwapTradingPairsQuery, config.Default.TradingPairSettings.Pancakeswap.MinLiquidity,
config.Default.TradingPairSettings.Pancakeswap.MinVol24,
config.Default.TradingPairSettings.Pancakeswap.MinTxCount24, forceInclude, primaryTokens)
if err != nil {
return err
}
pairs := make([][]TokenItem, 0)
chain := coin.Coins[coin.SMARTCHAIN]
for _, tradingPair := range tradingPairs {
tokenItem0, err := getTokenInfoFromSubgraphToken(chain, tradingPair.Token0)
if err != nil {
return err
}
tokenItem1, err := getTokenInfoFromSubgraphToken(chain, tradingPair.Token1)
if err != nil {
return err
}
if !isTokenPrimary(tradingPair.Token0, primaryTokens) {
tokenItem0, tokenItem1 = tokenItem1, tokenItem0
}
pairs = append(pairs, []TokenItem{*tokenItem0, *tokenItem1})
}
return rebuildTokenList(chain, pairs, forceExclude)
}
func retrievePairs(url string, query map[string]string, minLiquidity, minVol24, minTxCount24 int,
forceIncludeList []string, primaryTokens []string) ([]TradingPair, error) {
includeList := parseForceList(forceIncludeList)
pairs, err := fetchTradingPairs(url, query)
if err != nil {
return nil, err
}
filtered := make([]TradingPair, 0)
for _, pair := range pairs.Data.Pairs {
ok, err := checkTradingPairOK(pair, minLiquidity, minVol24, minTxCount24, primaryTokens, includeList)
if err != nil {
log.Debug(err)
}
if ok {
filtered = append(filtered, pair)
}
}
log.Debugf("Retrieved & filtered: %d", len(filtered))
return filtered, nil
}
func parseForceList(forceList []string) []ForceListPair {
result := make([]ForceListPair, 0, len(forceList))
for _, item := range forceList {
tokens := strings.Split(item, "-")
pair := ForceListPair{
Token0: tokens[0],
}
if len(tokens) >= 2 {
pair.Token1 = tokens[1]
}
result = append(result, pair)
}
return result
}
func fetchTradingPairs(url string, query map[string]string) (*TradingPairs, error) {
jsonValue, err := json.Marshal(query)
if err != nil {
return nil, err
}
log.WithField("url", url).Debug("Retrieving trading pair infos")
var result TradingPairs
err = http.PostHTTPResponse(url, jsonValue, &result)
if err != nil {
return nil, err
}
log.Debugf("Retrieved %d trading pair infos", len(result.Data.Pairs))
return &result, nil
}
func checkTradingPairOK(pair TradingPair, minLiquidity, minVol24, minTxCount24 int, primaryTokens []string,
forceIncludeList []ForceListPair) (bool, error) {
if pair.ID == "" || pair.ReserveUSD == "" || pair.VolumeUSD == "" || pair.TxCount == "" ||
pair.Token0 == nil || pair.Token1 == nil {
return false, nil
}
if !(isTokenPrimary(pair.Token0, primaryTokens) || isTokenPrimary(pair.Token1, primaryTokens)) {
log.Debugf("pair with no primary coin: %s -- %s", pair.Token0.Symbol, pair.Token1.Symbol)
return false, nil
}
if isPairMatchedToForceList(getTokenItemFromInfo(pair.Token0), getTokenItemFromInfo(pair.Token1), forceIncludeList) {
log.Debugf("pair included due to FORCE INCLUDE: %s -- %s", pair.Token0.Symbol, pair.Token1.Symbol)
return true, nil
}
reserveUSD, err := strconv.ParseFloat(pair.ReserveUSD, 64)
if err != nil {
return false, err
}
if int(reserveUSD) < minLiquidity {
log.Debugf("pair with low liquidity: %s -- %s", pair.Token0.Symbol, pair.Token1.Symbol)
return false, nil
}
volumeUSD, err := strconv.ParseFloat(pair.VolumeUSD, 64)
if err != nil {
return false, err
}
if int(volumeUSD) < minVol24 {
log.Debugf("pair with low volume: %s -- %s", pair.Token0.Symbol, pair.Token1.Symbol)
return false, nil
}
txCount, err := strconv.ParseFloat(pair.TxCount, 64)
if err != nil {
return false, err
}
if int(txCount) < minTxCount24 {
log.Debugf("pair with low tx count: %s -- %s", pair.Token0.Symbol, pair.Token1.Symbol)
return false, nil
}
return true, nil
}
func getTokenItemFromInfo(tokenInfo *TokenInfo) *TokenItem {
decimals, err := strconv.Atoi(tokenInfo.Decimals)
if err != nil {
return nil
}
return &TokenItem{
Asset: tokenInfo.ID,
Address: tokenInfo.ID,
Name: tokenInfo.Name,
Symbol: tokenInfo.Symbol,
Decimals: uint(decimals),
}
}
func getTokenInfoFromSubgraphToken(chain coin.Coin, token *TokenInfo) (*TokenItem, error) {
checksum, err := address.EIP55Checksum(token.ID)
if err != nil {
return nil, err
}
decimals, err := strconv.Atoi(token.Decimals)
if err != nil {
return nil, err
}
tokenType, ok := types.GetTokenType(chain.ID, token.ID)
if !ok {
return nil, fmt.Errorf("failed to get a token type for %s %s", chain.Symbol, token.ID)
}
return &TokenItem{
Asset: getAssetIDSymbol(checksum, chain.Symbol, chain.ID),
Type: types.TokenType(tokenType),
Address: checksum,
Name: token.Name,
Symbol: token.Symbol,
Decimals: uint(decimals),
LogoURI: getLogoURI(token.Symbol, chain.Handle, chain.Symbol),
Pairs: make([]Pair, 0),
}, nil
}
func isTokenPrimary(token *TokenInfo, primaryTokens []string) bool {
if token == nil {
return false
}
for _, primaryToken := range primaryTokens {
if strings.EqualFold(primaryToken, token.Symbol) {
return true
}
}
return false
}
func isPairMatchedToForceList(token0, token1 *TokenItem, forceIncludeList []ForceListPair) bool {
var matched bool
for _, forcePair := range forceIncludeList {
if matchPairToForceListEntry(token0, token1, forcePair) {
matched = true
}
}
return matched
}
func matchPairToForceListEntry(token0, token1 *TokenItem, forceListEntry ForceListPair) bool {
if forceListEntry.Token1 == "" {
if matchTokenToForceListEntry(token0, forceListEntry.Token0) ||
(token1 != nil && matchTokenToForceListEntry(token1, forceListEntry.Token0)) {
return true
}
return false
}
if token1 == nil {
return false
}
if matchTokenToForceListEntry(token0, forceListEntry.Token0) &&
matchTokenToForceListEntry(token0, forceListEntry.Token1) {
return true
}
if matchTokenToForceListEntry(token0, forceListEntry.Token1) &&
matchTokenToForceListEntry(token1, forceListEntry.Token0) {
return true
}
return false
}
func matchTokenToForceListEntry(token *TokenItem, forceListEntry string) bool {
if strings.EqualFold(forceListEntry, token.Symbol) ||
strings.EqualFold(forceListEntry, token.Asset) ||
strings.EqualFold(forceListEntry, token.Name) {
return true
}
return false
}
func matchPairToForceList(token0, token1 *TokenItem, forceList []ForceListPair) bool {
var matched bool
for _, forcePair := range forceList {
if matchPairToForceListEntry(token0, token1, forcePair) {
matched = true
}
}
return matched
}
func rebuildTokenList(chain coin.Coin, pairs [][]TokenItem, forceExcludeList []string) error {
if pairs == nil || len(pairs) < 5 {
return nil
}
excludeList := parseForceList(forceExcludeList)
pairs2 := make([][]TokenItem, 0)
for _, pair := range pairs {
if !checkTokenExists(chain.Handle, pair[0].Address) {
log.Debugf("pair with unsupported 1st coin: %s-%s", pair[0].Symbol, pair[1].Symbol)
continue
}
if !checkTokenExists(chain.Handle, pair[1].Address) {
log.Debugf("pair with unsupported 2nd coin: %s-%s", pair[0].Symbol, pair[1].Symbol)
continue
}
if matchPairToForceList(&pair[0], &pair[1], excludeList) {
log.Debugf("pair excluded due to FORCE EXCLUDE: %s-%s", pair[0].Symbol, pair[1].Symbol)
continue
}
pairs2 = append(pairs2, pair)
}
filteredCount := len(pairs) - len(pairs2)
log.Debugf("%d unsupported tokens filtered out, %d pairs", filteredCount, len(pairs2))
tokenListPath := path.GetTokenListPath(chain.Handle)
var list TokenList
err := fileLib.ReadJSONFile(tokenListPath, &list)
if err != nil {
return nil
}
log.Debugf("Tokenlist original: %d tokens", len(list.Tokens))
removeAllPairs(list.Tokens)
for _, pair := range pairs2 {
err = addPairIfNeeded(&pair[0], &pair[1], &list)
if err != nil {
return err
}
}
log.Debugf("Tokenlist updated: %d tokens", len(list.Tokens))
return createTokenListJSON(chain, list.Tokens)
}
func checkTokenExists(chain, tokenID string) bool {
logoPath := path.GetAssetLogoPath(chain, tokenID)
return fileLib.FileExists(logoPath)
}
func removeAllPairs(tokens []TokenItem) {
for i := range tokens {
tokens[i].Pairs = make([]Pair, 0)
}
}
func addPairIfNeeded(token0, token1 *TokenItem, list *TokenList) error {
err := addTokenIfNeeded(token0, list)
if err != nil {
return err
}
err = addTokenIfNeeded(token1, list)
if err != nil {
return err
}
addPairToToken(token1, token0, list)
return nil
}
func addTokenIfNeeded(token *TokenItem, list *TokenList) error {
for _, t := range list.Tokens {
if strings.EqualFold(t.Address, token.Address) {
return nil
}
}
err := updateTokenInfo(token)
if err != nil {
return err
}
list.Tokens = append(list.Tokens, *token)
return nil
}
func updateTokenInfo(token *TokenItem) error {
backendClient := backend.InitClient(config.Default.ClientURLs.BackendAPI, nil)
assetInfo, err := backendClient.GetAssetInfo(token.Asset)
if err != nil {
return fmt.Errorf("failed to get asset info for '%s': %w", token.Address, err)
}
if token.Name != assetInfo.Name {
log.Debugf("Token name adjusted: '%s' -> '%s'", token.Name, assetInfo.Name)
token.Name = assetInfo.Name
}
if token.Symbol != assetInfo.Symbol {
log.Debugf("Token symbol adjusted: '%s' -> '%s'", token.Symbol, assetInfo.Symbol)
token.Symbol = assetInfo.Symbol
}
if token.Decimals != uint(assetInfo.Decimals) {
log.Debugf("Token decimals adjusted: '%d' -> '%d'", token.Decimals, assetInfo.Decimals)
token.Decimals = uint(assetInfo.Decimals)
}
return nil
}
func addPairToToken(pairToken, token *TokenItem, list *TokenList) {
var tokenInListIndex = -1
for i, t := range list.Tokens {
if t.Address == token.Address {
tokenInListIndex = i
break
}
}
if tokenInListIndex == -1 {
return
}
if list.Tokens[tokenInListIndex].Pairs == nil {
list.Tokens[tokenInListIndex].Pairs = make([]Pair, 0)
}
for _, pair := range list.Tokens[tokenInListIndex].Pairs {
if pair.Base == pairToken.Asset {
return
}
}
list.Tokens[tokenInListIndex].Pairs = append(list.Tokens[tokenInListIndex].Pairs, Pair{Base: pairToken.Asset})
}