| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343 |
- package p
- import (
- "encoding/json"
- "fmt"
- "io/ioutil"
- "log"
- "os"
- "strings"
- "time"
- "net/http"
- "github.com/certusone/wormhole/node/pkg/vaa"
- )
- const cgBaseUrl = "https://api.coingecko.com/api/v3/"
- const cgProBaseUrl = "https://pro-api.coingecko.com/api/v3/"
- type CoinGeckoCoin struct {
- Id string `json:"id"`
- Symbol string `json:"symbol"`
- Name string `json:"name"`
- }
- type CoinGeckoCoins []CoinGeckoCoin
- type CoinGeckoMarket [2]float64
- type CoinGeckoMarketRes struct {
- Prices []CoinGeckoMarket `json:"prices"`
- }
- type CoinGeckoErrorRes struct {
- Error string `json:"error"`
- }
- func fetchCoinGeckoCoins() map[string][]CoinGeckoCoin {
- baseUrl := cgBaseUrl
- cgApiKey := os.Getenv("COINGECKO_API_KEY")
- if cgApiKey != "" {
- baseUrl = cgProBaseUrl
- }
- url := fmt.Sprintf("%vcoins/list", baseUrl)
- req, reqErr := http.NewRequest("GET", url, nil)
- if reqErr != nil {
- log.Fatalf("failed coins request, err: %v", reqErr)
- }
- if cgApiKey != "" {
- req.Header.Set("X-Cg-Pro-Api-Key", cgApiKey)
- }
- res, resErr := http.DefaultClient.Do(req)
- if resErr != nil {
- log.Fatalf("failed get coins response, err: %v", resErr)
- }
- defer res.Body.Close()
- body, bodyErr := ioutil.ReadAll(res.Body)
- if bodyErr != nil {
- log.Fatalf("failed decoding coins body, err: %v", bodyErr)
- }
- var parsed []CoinGeckoCoin
- parseErr := json.Unmarshal(body, &parsed)
- if parseErr != nil {
- log.Printf("failed parsing body. err %v\n", parseErr)
- }
- var geckoCoins = map[string][]CoinGeckoCoin{}
- for _, coin := range parsed {
- symbol := strings.ToLower(coin.Symbol)
- geckoCoins[symbol] = append(geckoCoins[symbol], coin)
- }
- return geckoCoins
- }
- func chainIdToCoinGeckoPlatform(chain vaa.ChainID) string {
- switch chain {
- case vaa.ChainIDSolana:
- return "solana"
- case vaa.ChainIDEthereum:
- return "ethereum"
- case vaa.ChainIDTerra:
- return "terra"
- case vaa.ChainIDBSC:
- return "binance-smart-chain"
- case vaa.ChainIDPolygon:
- return "polygon-pos"
- }
- return ""
- }
- func fetchCoinGeckoCoinFromContract(chainId vaa.ChainID, address string) CoinGeckoCoin {
- baseUrl := cgBaseUrl
- cgApiKey := os.Getenv("COINGECKO_API_KEY")
- if cgApiKey != "" {
- baseUrl = cgProBaseUrl
- }
- platform := chainIdToCoinGeckoPlatform(chainId)
- url := fmt.Sprintf("%vcoins/%v/contract/%v", baseUrl, platform, address)
- req, reqErr := http.NewRequest("GET", url, nil)
- if reqErr != nil {
- log.Fatalf("failed contract request, err: %v\n", reqErr)
- }
- if cgApiKey != "" {
- req.Header.Set("X-Cg-Pro-Api-Key", cgApiKey)
- }
- res, resErr := http.DefaultClient.Do(req)
- if resErr != nil {
- log.Fatalf("failed get contract response, err: %v\n", resErr)
- }
- defer res.Body.Close()
- body, bodyErr := ioutil.ReadAll(res.Body)
- if bodyErr != nil {
- log.Fatalf("failed decoding contract body, err: %v\n", bodyErr)
- }
- var parsed CoinGeckoCoin
- parseErr := json.Unmarshal(body, &parsed)
- if parseErr != nil {
- log.Printf("failed parsing body. err %v\n", parseErr)
- var errRes CoinGeckoErrorRes
- if err := json.Unmarshal(body, &errRes); err == nil {
- if errRes.Error == "Could not find coin with the given id" {
- log.Printf("Could not find CoinGecko coin by contract address, for chain %v, address, %v\n", chainId, address)
- } else {
- log.Println("Failed calling CoinGecko, got err", errRes.Error)
- }
- }
- }
- return parsed
- }
- func fetchCoinGeckoCoinId(chainId vaa.ChainID, address, symbol, name string) (coinId, foundSymbol, foundName string) {
- // try coingecko, return if good
- // if coingecko does not work, try chain-specific options
- // initialize strings that will be returned if we find a symbol/name
- // when looking up this token by contract address
- newSymbol := ""
- newName := ""
- if symbol == "" && chainId == vaa.ChainIDSolana {
- // try to lookup the symbol in solana token list, from the address
- if token, ok := solanaTokens[address]; ok {
- symbol = token.Symbol
- name = token.Name
- newSymbol = token.Symbol
- newName = token.Name
- }
- }
- if _, ok := coinGeckoCoins[strings.ToLower(symbol)]; ok {
- tokens := coinGeckoCoins[strings.ToLower(symbol)]
- if len(tokens) == 1 {
- // only one match found for this symbol
- return tokens[0].Id, newSymbol, newName
- }
- for _, token := range tokens {
- if token.Name == name {
- // found token by name match
- return token.Id, newSymbol, newName
- }
- if strings.Contains(strings.ToLower(strings.ReplaceAll(name, " ", "")), strings.ReplaceAll(token.Id, "-", "")) {
- // found token by id match
- log.Println("found token by symbol and name match", name)
- return token.Id, newSymbol, newName
- }
- }
- // more than one symbol with this name, let contract lookup try
- }
- coin := fetchCoinGeckoCoinFromContract(chainId, address)
- if coin.Id != "" {
- return coin.Id, newSymbol, newName
- }
- // could not find a CoinGecko coin
- return "", newSymbol, newName
- }
- func fetchCoinGeckoPrice(coinId string, timestamp time.Time) (float64, error) {
- hourAgo := time.Now().Add(-time.Duration(1) * time.Hour)
- withinLastHour := timestamp.After(hourAgo)
- start, end := rangeFromTime(timestamp, 4)
- baseUrl := cgBaseUrl
- cgApiKey := os.Getenv("COINGECKO_API_KEY")
- if cgApiKey != "" {
- baseUrl = cgProBaseUrl
- }
- url := fmt.Sprintf("%vcoins/%v/market_chart/range?vs_currency=usd&from=%v&to=%v", baseUrl, coinId, start.Unix(), end.Unix())
- req, reqErr := http.NewRequest("GET", url, nil)
- if reqErr != nil {
- log.Fatalf("failed coins request, err: %v\n", reqErr)
- }
- if cgApiKey != "" {
- req.Header.Set("X-Cg-Pro-Api-Key", cgApiKey)
- }
- res, resErr := http.DefaultClient.Do(req)
- if resErr != nil {
- log.Fatalf("failed get coins response, err: %v\n", resErr)
- }
- defer res.Body.Close()
- body, bodyErr := ioutil.ReadAll(res.Body)
- if bodyErr != nil {
- log.Fatalf("failed decoding coins body, err: %v\n", bodyErr)
- }
- var parsed CoinGeckoMarketRes
- parseErr := json.Unmarshal(body, &parsed)
- if parseErr != nil {
- log.Printf("failed parsing body. err %v\n", parseErr)
- var errRes CoinGeckoErrorRes
- if err := json.Unmarshal(body, &errRes); err == nil {
- log.Println("Failed calling CoinGecko, got err", errRes.Error)
- }
- }
- if len(parsed.Prices) >= 1 {
- var priceIndex int
- if withinLastHour {
- // use the last price in the list, latest price
- priceIndex = len(parsed.Prices) - 1
- } else {
- // use a price from the middle of the list, as that should be
- // closest to the timestamp.
- numPrices := len(parsed.Prices)
- priceIndex = numPrices / 2
- }
- price := parsed.Prices[priceIndex][1]
- log.Printf("found a price of $%f for %v!\n", price, coinId)
- return price, nil
- }
- log.Println("no price found in coinGecko for", coinId)
- return 0, fmt.Errorf("no price found for %v", coinId)
- }
- const solanaTokenListURL = "https://raw.githubusercontent.com/solana-labs/token-list/main/src/tokens/solana.tokenlist.json"
- type SolanaToken struct {
- Address string `json:"address"`
- Symbol string `json:"symbol"`
- Name string `json:"name"`
- Decimals int `json:"decimals"`
- }
- type SolanaTokenListRes struct {
- Tokens []SolanaToken `json:"tokens"`
- }
- func fetchSolanaTokenList() map[string]SolanaToken {
- req, reqErr := http.NewRequest("GET", solanaTokenListURL, nil)
- if reqErr != nil {
- log.Fatalf("failed solana token list request, err: %v", reqErr)
- }
- res, resErr := http.DefaultClient.Do(req)
- if resErr != nil {
- log.Fatalf("failed get solana token list response, err: %v", resErr)
- }
- defer res.Body.Close()
- body, bodyErr := ioutil.ReadAll(res.Body)
- if bodyErr != nil {
- log.Fatalf("failed decoding solana token list body, err: %v", bodyErr)
- }
- var parsed SolanaTokenListRes
- parseErr := json.Unmarshal(body, &parsed)
- if parseErr != nil {
- log.Printf("failed parsing body. err %v\n", parseErr)
- }
- var solTokens = map[string]SolanaToken{}
- for _, token := range parsed.Tokens {
- if _, ok := solTokens[token.Address]; !ok {
- solTokens[token.Address] = token
- }
- }
- return solTokens
- }
- const solanaBeachPublicBaseURL = "https://prod-api.solana.surf/v1/"
- const solanaBeachPrivateBaseURL = "https://api.solanabeach.io/v1/"
- type SolanaBeachAccountOwner struct {
- Owner SolanaBeachAccountOwnerAddress `json:"owner"`
- }
- type SolanaBeachAccountOwnerAddress struct {
- Address string `json:"address"`
- }
- type SolanaBeachAccountResponse struct {
- Value struct {
- Extended struct {
- SolanaBeachAccountOwner
- } `json:"extended"`
- } `json:"value"`
- }
- func fetchSolanaAccountOwner(account string) string {
- baseUrl := solanaBeachPublicBaseURL
- sbApiKey := os.Getenv("SOLANABEACH_API_KEY")
- if sbApiKey != "" {
- baseUrl = solanaBeachPrivateBaseURL
- }
- url := fmt.Sprintf("%vaccount/%v", baseUrl, account)
- req, reqErr := http.NewRequest("GET", url, nil)
- if reqErr != nil {
- log.Fatalf("failed solanabeach request, err: %v", reqErr)
- }
- if sbApiKey != "" {
- req.Header.Set("Authorization", fmt.Sprintf("Bearer %v", sbApiKey))
- }
- res, resErr := http.DefaultClient.Do(req)
- if resErr != nil {
- log.Fatalf("failed get solana beach account response, err: %v", resErr)
- }
- defer res.Body.Close()
- body, bodyErr := ioutil.ReadAll(res.Body)
- if bodyErr != nil {
- log.Fatalf("failed decoding solana beach account body, err: %v", bodyErr)
- }
- var parsed SolanaBeachAccountResponse
- parseErr := json.Unmarshal(body, &parsed)
- if parseErr != nil {
- log.Printf("failed parsing body. err %v\n", parseErr)
- }
- address := parsed.Value.Extended.Owner.Address
- log.Println("got owner address from Solana Beach! ", address)
- return address
- }
|