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}) }