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 }