github.com/diadata-org/diadata@v1.4.593/internal/pkg/supplyService/supply.go (about)

     1  package supplyservice
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"fmt"
     7  	"io/ioutil"
     8  	"math"
     9  	"math/big"
    10  	"os"
    11  	"time"
    12  
    13  	"github.com/diadata-org/diadata/pkg/dia"
    14  	utils "github.com/diadata-org/diadata/pkg/utils"
    15  	"github.com/ethereum/go-ethereum/accounts/abi/bind"
    16  	"github.com/ethereum/go-ethereum/common"
    17  	"github.com/ethereum/go-ethereum/ethclient"
    18  	log "github.com/sirupsen/logrus"
    19  )
    20  
    21  var (
    22  	numMaxRetryCG    = 4
    23  	waitRetrySeconds = 60
    24  	apiKey           = utils.Getenv("COINGECKO_SUPPLY_API_KEY", "")
    25  )
    26  
    27  type CGCoin struct {
    28  	Address    string       `json:"contract_address"`
    29  	MarketData CGMarketData `json:"market_data"`
    30  	Platform   string       `json:"asset_platform_id"`
    31  }
    32  
    33  type CGMarketData struct {
    34  	TotalSupply       float64 `json:"total_supply"`
    35  	CirculatingSupply float64 `json:"circulating_supply"`
    36  }
    37  
    38  func GetETHSuppliesFromCG() (supplies []dia.Supply, err error) {
    39  	IDs, err := getAllIDsCG()
    40  	if err != nil {
    41  		return
    42  	}
    43  	for _, ID := range IDs {
    44  		retries := 0
    45  		var coin CGCoin
    46  		var err error
    47  		var status int
    48  		for retries < numMaxRetryCG {
    49  			coin, status, err = getCGCoinInfo(ID)
    50  			if err != nil {
    51  				if status == 429 {
    52  					time.Sleep(time.Duration(waitRetrySeconds) * time.Second)
    53  					log.Info("rate limitation: sleep")
    54  					retries++
    55  				} else {
    56  					log.Error("get coin info: ", err)
    57  					break
    58  				}
    59  			} else {
    60  				time.Sleep(1000 * time.Millisecond)
    61  				break
    62  			}
    63  		}
    64  		if coin.Address != "" && coin.Platform == "ethereum" {
    65  			supply := dia.Supply{
    66  				Asset: dia.Asset{
    67  					Address:    common.HexToAddress(coin.Address).Hex(),
    68  					Blockchain: dia.ETHEREUM,
    69  				},
    70  				Supply:            coin.MarketData.TotalSupply,
    71  				CirculatingSupply: coin.MarketData.CirculatingSupply,
    72  			}
    73  			supplies = append(supplies, supply)
    74  		}
    75  	}
    76  	return
    77  }
    78  
    79  func getCGCoinInfo(id string) (coin CGCoin, status int, err error) {
    80  	var resp []byte
    81  	resp, status, err = utils.GetRequest("https://pro-api.coingecko.com/api/v3/coins/" + id + "?x_cg_pro_api_key=" + apiKey)
    82  	if err != nil {
    83  		return
    84  	}
    85  	err = json.Unmarshal(resp, &coin)
    86  	if err != nil {
    87  		return
    88  	}
    89  	if id == "ethereum" {
    90  		coin.Address = "0x0000000000000000000000000000000000000000"
    91  		coin.Platform = "ethereum"
    92  	}
    93  	return
    94  }
    95  
    96  func getAllIDsCG() (IDs []string, err error) {
    97  	resp, _, err := utils.GetRequest("https://pro-api.coingecko.com/api/v3/coins/list?x_cg_pro_api_key=" + apiKey)
    98  	if err != nil {
    99  		log.Error("getAllIDsCG: ", err)
   100  		return
   101  	}
   102  	var data []interface{}
   103  	err = json.Unmarshal(resp, &data)
   104  	if err != nil {
   105  		return
   106  	}
   107  	for _, item := range data {
   108  		token := item.(map[string]interface{})
   109  		IDs = append(IDs, token["id"].(string))
   110  	}
   111  	return
   112  }
   113  
   114  // GetLockedWalletsFromConfig returns a map which maps an asset to the list of its locked wallets
   115  func GetLockedWalletsFromConfig(filename string) (allAssetsMap map[string][]string, err error) {
   116  
   117  	var jsonFile *os.File
   118  
   119  	executionMode := os.Getenv("EXEC_MODE")
   120  	if executionMode == "production" {
   121  		jsonFile, err = os.Open(fmt.Sprintf("/config/token_supply/%s.json", filename))
   122  	} else {
   123  		jsonFile, err = os.Open(fmt.Sprintf("../../../config/token_supply/%s.json", filename))
   124  	}
   125  	if err != nil {
   126  		log.Errorln("Error opening file", err)
   127  		return
   128  	}
   129  	defer func() {
   130  		cerr := jsonFile.Close()
   131  		if err == nil {
   132  			err = cerr
   133  		}
   134  	}()
   135  
   136  	byteData, err := ioutil.ReadAll(jsonFile)
   137  	if err != nil {
   138  		log.Error(err)
   139  		return
   140  	}
   141  
   142  	type lockedAsset struct {
   143  		Address       string   `json:"asset"`
   144  		LockedWallets []string `json:"wallets"`
   145  	}
   146  	type lockedAssetList struct {
   147  		AllAssets []lockedAsset `json:"lockedWallets"`
   148  	}
   149  	var allAssets lockedAssetList
   150  	err = json.Unmarshal(byteData, &allAssets)
   151  	if err != nil {
   152  		return
   153  	}
   154  
   155  	// make map[string][]string from allAssets. This accounts for erroneous addition of new entry
   156  	// for already existing asset in config file.
   157  	var diff []string
   158  	for _, asset := range allAssets.AllAssets {
   159  		if _, ok := allAssetsMap[asset.Address]; !ok {
   160  			allAssetsMap[asset.Address] = asset.LockedWallets
   161  		} else {
   162  			diff = utils.SliceDifference(asset.LockedWallets, allAssetsMap[asset.Address])
   163  			allAssetsMap[asset.Address] = append(allAssetsMap[asset.Address], diff...)
   164  		}
   165  	}
   166  
   167  	return
   168  }
   169  
   170  // GetWalletBalance returns balance of token with address @tokenAddr in wallet with address @walletAddr
   171  func GetWalletBalance(walletAddr string, tokenAddr string, c *ethclient.Client) (balance float64, err error) {
   172  	instance, err := NewERC20(common.HexToAddress(tokenAddr), c)
   173  	if err != nil {
   174  		log.Fatal(err)
   175  		return
   176  	}
   177  	decimals, err := instance.Decimals(&bind.CallOpts{})
   178  	if err != nil {
   179  		log.Error(err)
   180  		return
   181  	}
   182  
   183  	walletBal, err := instance.BalanceOf(&bind.CallOpts{}, common.HexToAddress(walletAddr))
   184  	if err != nil {
   185  		log.Errorf("failed to retrieve token owner balance from wallet %s: %v \n", walletAddr, err)
   186  		return
   187  	}
   188  
   189  	fbal := new(big.Float)
   190  	fbal.SetString(walletBal.String())
   191  
   192  	balance, _ = new(big.Float).Quo(fbal, big.NewFloat(math.Pow10(int(decimals)))).Float64()
   193  	return
   194  }
   195  
   196  // GetTotalSupplyfromMainNet returns total supply minus wallets' balances from list of wallets
   197  func GetTotalSupplyfromMainNet(tokenAddress string, lockedWallets []string, client *ethclient.Client) (supply dia.Supply, err error) {
   198  
   199  	instance, err := NewERC20(common.HexToAddress(tokenAddress), client)
   200  	if err != nil {
   201  		log.Error("error getting token contract: ", err)
   202  		return
   203  	}
   204  
   205  	totalSupply, err := instance.TotalSupply(&bind.CallOpts{})
   206  	if err != nil {
   207  		return
   208  	}
   209  	symbol, err := instance.Symbol(&bind.CallOpts{})
   210  	if err != nil {
   211  		return
   212  	}
   213  	name, err := instance.Name(&bind.CallOpts{})
   214  	if err != nil {
   215  		return
   216  	}
   217  	decimals, err := instance.Decimals(&bind.CallOpts{})
   218  	if err != nil {
   219  		return
   220  	}
   221  
   222  	// Get total supply
   223  	fbal := new(big.Float)
   224  	fbal.SetString(totalSupply.String())
   225  	valuei := new(big.Float).Quo(fbal, big.NewFloat(math.Pow10(int(decimals))))
   226  	totalSupp, _ := valuei.Float64()
   227  
   228  	// Subtract locked wallets' balances from total supply for circulating supply
   229  	circulatingSupply := totalSupp
   230  	for _, walletAddress := range lockedWallets {
   231  		var balance float64
   232  		balance, err = GetWalletBalance(walletAddress, tokenAddress, client)
   233  		if err != nil {
   234  			log.Errorf("error getting wallet balance for wallet %s \n", walletAddress)
   235  		}
   236  		circulatingSupply = circulatingSupply - balance
   237  	}
   238  
   239  	header, err := client.HeaderByNumber(context.Background(), nil)
   240  	if err != nil {
   241  		log.Fatal(err)
   242  	}
   243  	asset := dia.Asset{
   244  		Symbol:     symbol,
   245  		Name:       name,
   246  		Decimals:   decimals,
   247  		Address:    common.HexToAddress(tokenAddress).Hex(),
   248  		Blockchain: dia.ETHEREUM,
   249  	}
   250  	supply = dia.Supply{
   251  		Asset:             asset,
   252  		Supply:            totalSupp,
   253  		CirculatingSupply: circulatingSupply,
   254  		Source:            "diadata.org",
   255  		Time:              time.Unix(int64(header.Time), 0),
   256  	}
   257  
   258  	return
   259  }