mirror of
https://github.com/Instadapp/trustwallet-assets.git
synced 2024-07-29 22:37:31 +00:00
4c38c9aed4
* Add token status checking * Fix binance tokenlist generating * Remove abandoned tokens from eth tokenlist
533 lines
14 KiB
Go
533 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))
|
|
|
|
sortTokens(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})
|
|
}
|