trustwallet-assets/internal/processor/updaters_auto.go
Daniel 72d5b319d4
Add address checksum validation for tokenlist files (#17416)
* Add address checksum validation for tokenlist files

* Remove updaters_manual.go, move tokenlist validation to assets-go-libs
2022-01-16 17:13:01 +03:00

323 lines
8.0 KiB
Go

package processor
import (
"bytes"
"encoding/json"
"fmt"
"os"
"reflect"
"sort"
"strconv"
"time"
log "github.com/sirupsen/logrus"
fileLib "github.com/trustwallet/assets-go-libs/file"
"github.com/trustwallet/assets-go-libs/image"
"github.com/trustwallet/assets-go-libs/path"
"github.com/trustwallet/assets-go-libs/validation/info"
"github.com/trustwallet/assets-go-libs/validation/tokenlist"
"github.com/trustwallet/assets/internal/config"
"github.com/trustwallet/go-libs/blockchain/binance"
"github.com/trustwallet/go-libs/blockchain/binance/explorer"
assetlib "github.com/trustwallet/go-primitives/asset"
"github.com/trustwallet/go-primitives/coin"
"github.com/trustwallet/go-primitives/numbers"
"github.com/trustwallet/go-primitives/types"
)
const (
assetsPage = 1
assetsRows = 1000
marketPairsLimit = 1000
tokensListLimit = 10000
twLogoURL = "https://trustwallet.com/assets/images/favicon.png"
timestampFormat = "2006-01-02T15:04:05.000000"
activeStatus = "active"
)
func (s *Service) UpdateBinanceTokens() error {
explorerClient := explorer.InitClient(config.Default.ClientURLs.Binance.Explorer, nil)
bep2AssetList, err := explorerClient.FetchBep2Assets(assetsPage, assetsRows)
if err != nil {
return err
}
dexClient := binance.InitClient(config.Default.ClientURLs.Binance.Dex, "", nil)
marketPairs, err := dexClient.FetchMarketPairs(marketPairsLimit)
if err != nil {
return err
}
tokenList, err := dexClient.FetchTokens(tokensListLimit)
if err != nil {
return err
}
chain, err := types.GetChainFromAssetType(string(types.BEP2))
if err != nil {
return err
}
err = fetchMissingAssets(chain, bep2AssetList.AssetInfoList)
if err != nil {
return err
}
tokens, err := generateTokenList(marketPairs, tokenList)
if err != nil {
return err
}
sortTokens(tokens)
return createTokenListJSON(chain, tokens)
}
func fetchMissingAssets(chain coin.Coin, assets []explorer.Bep2Asset) error {
for _, a := range assets {
if a.AssetImg == "" || a.Decimals == 0 {
continue
}
assetLogoPath := path.GetAssetLogoPath(chain.Handle, a.Asset)
if fileLib.FileExists(assetLogoPath) {
continue
}
if err := createLogo(assetLogoPath, a); err != nil {
return err
}
if err := createInfoJSON(chain, a); err != nil {
return err
}
}
return nil
}
func createLogo(assetLogoPath string, a explorer.Bep2Asset) error {
err := fileLib.CreateDirPath(assetLogoPath)
if err != nil {
return err
}
return image.CreatePNGFromURL(a.AssetImg, assetLogoPath)
}
func createInfoJSON(chain coin.Coin, a explorer.Bep2Asset) error {
explorerURL, err := coin.GetCoinExploreURL(chain, a.Asset)
if err != nil {
return err
}
assetType := string(types.BEP2)
website := ""
description := "-"
status := activeStatus
assetInfo := info.AssetModel{
Name: &a.Name,
Type: &assetType,
Symbol: &a.MappedAsset,
Decimals: &a.Decimals,
Website: &website,
Description: &description,
Explorer: &explorerURL,
Status: &status,
ID: &a.Asset,
}
assetInfoPath := path.GetAssetInfoPath(chain.Handle, a.Asset)
return fileLib.CreateJSONFile(assetInfoPath, &assetInfo)
}
func createTokenListJSON(chain coin.Coin, tokens []tokenlist.Token) error {
tokenListPath := path.GetTokenListPath(chain.Handle)
var oldTokenList tokenlist.Model
err := fileLib.ReadJSONFile(tokenListPath, &oldTokenList)
if err != nil {
return nil
}
if reflect.DeepEqual(oldTokenList.Tokens, tokens) {
return nil
}
if len(tokens) == 0 {
return nil
}
log.Debugf("Tokenlist: list with %d tokens and %d pairs written to %s.",
len(tokens), countTotalPairs(tokens), tokenListPath)
return fileLib.CreateJSONFile(tokenListPath, &tokenlist.Model{
Name: fmt.Sprintf("Trust Wallet: %s", coin.Coins[chain.ID].Name),
LogoURI: twLogoURL,
Timestamp: time.Now().Format(timestampFormat),
Tokens: tokens,
Version: tokenlist.Version{Major: oldTokenList.Version.Major + 1},
})
}
func countTotalPairs(tokens []tokenlist.Token) int {
var counter int
for _, token := range tokens {
counter += len(token.Pairs)
}
return counter
}
func sortTokens(tokens []tokenlist.Token) {
sort.Slice(tokens, func(i, j int) bool {
if len(tokens[i].Pairs) != len(tokens[j].Pairs) {
return len(tokens[i].Pairs) > len(tokens[j].Pairs)
}
return tokens[i].Address < tokens[j].Address
})
for _, token := range tokens {
sort.Slice(token.Pairs, func(i, j int) bool {
return token.Pairs[i].Base < token.Pairs[j].Base
})
}
}
func generateTokenList(marketPairs []binance.MarketPair, tokenList binance.Tokens) ([]tokenlist.Token, error) {
if len(marketPairs) < 5 {
return nil, fmt.Errorf("no markets info is returned from Binance DEX: %d", len(marketPairs))
}
if len(tokenList) < 5 {
return nil, fmt.Errorf("no tokens info is returned from Binance DEX: %d", len(tokenList))
}
pairsMap := make(map[string][]tokenlist.Pair)
pairsList := make(map[string]struct{})
tokensMap := make(map[string]binance.Token)
for _, token := range tokenList {
tokensMap[token.Symbol] = token
}
for _, marketPair := range marketPairs {
if !isTokenExistOrActive(marketPair.BaseAssetSymbol) || !isTokenExistOrActive(marketPair.QuoteAssetSymbol) {
continue
}
tokenSymbol := marketPair.QuoteAssetSymbol
if val, exists := pairsMap[tokenSymbol]; exists {
val = append(val, getPair(marketPair))
pairsMap[tokenSymbol] = val
} else {
pairsMap[tokenSymbol] = []tokenlist.Pair{getPair(marketPair)}
}
pairsList[marketPair.BaseAssetSymbol] = struct{}{}
pairsList[marketPair.QuoteAssetSymbol] = struct{}{}
}
tokenItems := make([]tokenlist.Token, 0, len(pairsList))
for pair := range pairsList {
token := tokensMap[pair]
var pairs []tokenlist.Pair
pairs, exists := pairsMap[token.Symbol]
if !exists {
pairs = make([]tokenlist.Pair, 0)
}
tokenItems = append(tokenItems, tokenlist.Token{
Asset: getAssetIDSymbol(token.Symbol, coin.Coins[coin.BINANCE].Symbol, coin.BINANCE),
Type: getTokenType(token.Symbol, coin.Coins[coin.BINANCE].Symbol, types.BEP2),
Address: token.Symbol,
Name: token.Name,
Symbol: token.OriginalSymbol,
Decimals: coin.Coins[coin.BINANCE].Decimals,
LogoURI: getLogoURI(token.Symbol, coin.Coins[coin.BINANCE].Handle, coin.Coins[coin.BINANCE].Symbol),
Pairs: pairs,
})
}
return tokenItems, nil
}
func isTokenExistOrActive(symbol string) bool {
if symbol == coin.Coins[coin.BINANCE].Symbol {
return true
}
assetPath := path.GetAssetInfoPath(coin.Coins[coin.BINANCE].Handle, symbol)
infoFile, err := os.Open(assetPath)
if err != nil {
log.Debugf("asset file open error: %s", err.Error())
return false
}
buf := bytes.NewBuffer(nil)
if _, err = buf.ReadFrom(infoFile); err != nil {
log.Debugf("buffer read error: %s", err.Error())
return false
}
infoFile.Close()
var infoAsset info.AssetModel
err = json.Unmarshal(buf.Bytes(), &infoAsset)
if err != nil {
log.Debugf("json unmarshalling error: %s", err.Error())
return false
}
if infoAsset.GetStatus() != activeStatus {
log.Debugf("asset status [%s] is not active", symbol)
return false
}
return true
}
func getPair(marketPair binance.MarketPair) tokenlist.Pair {
return tokenlist.Pair{
Base: getAssetIDSymbol(marketPair.BaseAssetSymbol, coin.Coins[coin.BINANCE].Symbol, coin.BINANCE),
LotSize: strconv.FormatInt(numbers.ToSatoshi(marketPair.LotSize), 10),
TickSize: strconv.FormatInt(numbers.ToSatoshi(marketPair.TickSize), 10),
}
}
func getAssetIDSymbol(tokenID string, nativeCoinID string, coinType uint) string {
if tokenID == nativeCoinID {
return assetlib.BuildID(coinType, "")
}
return assetlib.BuildID(coinType, tokenID)
}
func getTokenType(symbol string, nativeCoinSymbol string, tokenType types.TokenType) types.TokenType {
if symbol == nativeCoinSymbol {
return types.Coin
}
return tokenType
}
func getLogoURI(id, githubChainFolder, nativeCoinSymbol string) string {
if id == nativeCoinSymbol {
return path.GetChainLogoURL(config.Default.URLs.TWAssetsApp, githubChainFolder)
}
return path.GetAssetLogoURL(config.Default.URLs.TWAssetsApp, githubChainFolder, id)
}