2021-11-16 10:20:20 -08:00
|
|
|
package p
|
|
|
|
|
|
|
|
import (
|
2022-02-23 06:01:47 -08:00
|
|
|
"context"
|
2021-11-16 10:20:20 -08:00
|
|
|
"encoding/json"
|
|
|
|
"fmt"
|
|
|
|
"io/ioutil"
|
|
|
|
"log"
|
2021-11-19 11:13:41 -08:00
|
|
|
"os"
|
2021-11-19 11:09:39 -08:00
|
|
|
"strings"
|
2021-11-16 10:20:20 -08:00
|
|
|
"time"
|
|
|
|
|
|
|
|
"net/http"
|
2021-11-19 11:09:39 -08:00
|
|
|
|
|
|
|
"github.com/certusone/wormhole/node/pkg/vaa"
|
2021-11-16 10:20:20 -08:00
|
|
|
)
|
|
|
|
|
|
|
|
const cgBaseUrl = "https://api.coingecko.com/api/v3/"
|
2021-11-19 11:13:41 -08:00
|
|
|
const cgProBaseUrl = "https://pro-api.coingecko.com/api/v3/"
|
2021-11-16 10:20:20 -08:00
|
|
|
|
|
|
|
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"`
|
|
|
|
}
|
2021-11-19 11:09:39 -08:00
|
|
|
type CoinGeckoErrorRes struct {
|
|
|
|
Error string `json:"error"`
|
|
|
|
}
|
2021-11-16 10:20:20 -08:00
|
|
|
|
|
|
|
func fetchCoinGeckoCoins() map[string][]CoinGeckoCoin {
|
2022-03-17 15:01:35 -07:00
|
|
|
defer timeTrack(time.Now(), "fetchCoinGeckoCoins")
|
2021-11-19 11:13:41 -08:00
|
|
|
baseUrl := cgBaseUrl
|
|
|
|
cgApiKey := os.Getenv("COINGECKO_API_KEY")
|
|
|
|
if cgApiKey != "" {
|
|
|
|
baseUrl = cgProBaseUrl
|
|
|
|
}
|
|
|
|
url := fmt.Sprintf("%vcoins/list", baseUrl)
|
2021-11-16 10:20:20 -08:00
|
|
|
req, reqErr := http.NewRequest("GET", url, nil)
|
|
|
|
if reqErr != nil {
|
|
|
|
log.Fatalf("failed coins request, err: %v", reqErr)
|
|
|
|
}
|
|
|
|
|
2021-11-19 11:13:41 -08:00
|
|
|
if cgApiKey != "" {
|
|
|
|
req.Header.Set("X-Cg-Pro-Api-Key", cgApiKey)
|
|
|
|
}
|
|
|
|
|
2021-11-16 10:20:20 -08:00
|
|
|
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 {
|
2022-03-02 14:37:05 -08:00
|
|
|
log.Printf("fetchCoinGeckoCoins failed parsing body. err %v\n", parseErr)
|
2021-11-16 10:20:20 -08:00
|
|
|
}
|
|
|
|
var geckoCoins = map[string][]CoinGeckoCoin{}
|
|
|
|
for _, coin := range parsed {
|
2021-11-19 11:09:39 -08:00
|
|
|
symbol := strings.ToLower(coin.Symbol)
|
|
|
|
geckoCoins[symbol] = append(geckoCoins[symbol], coin)
|
2021-11-16 10:20:20 -08:00
|
|
|
}
|
|
|
|
return geckoCoins
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2021-11-19 11:09:39 -08:00
|
|
|
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"
|
2022-02-27 08:08:31 -08:00
|
|
|
case vaa.ChainIDAvalanche:
|
|
|
|
return "avalanche"
|
|
|
|
case vaa.ChainIDOasis:
|
|
|
|
return "oasis"
|
|
|
|
case vaa.ChainIDAlgorand:
|
|
|
|
return "algorand"
|
2022-04-13 16:55:34 -07:00
|
|
|
case vaa.ChainIDAurora:
|
|
|
|
return "aurora"
|
2022-02-27 08:08:31 -08:00
|
|
|
case vaa.ChainIDFantom:
|
|
|
|
return "fantom"
|
2022-04-13 16:55:34 -07:00
|
|
|
case vaa.ChainIDKarura:
|
|
|
|
return "" // no platform_id for karura on CG
|
|
|
|
case vaa.ChainIDAcala:
|
|
|
|
return "polkadot"
|
2022-02-27 08:08:31 -08:00
|
|
|
case vaa.ChainIDEthereumRopsten:
|
|
|
|
return "ethereum"
|
2021-11-19 11:09:39 -08:00
|
|
|
}
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
|
|
|
|
func fetchCoinGeckoCoinFromContract(chainId vaa.ChainID, address string) CoinGeckoCoin {
|
2021-11-19 11:13:41 -08:00
|
|
|
baseUrl := cgBaseUrl
|
|
|
|
cgApiKey := os.Getenv("COINGECKO_API_KEY")
|
|
|
|
if cgApiKey != "" {
|
|
|
|
baseUrl = cgProBaseUrl
|
|
|
|
}
|
2021-11-19 11:09:39 -08:00
|
|
|
platform := chainIdToCoinGeckoPlatform(chainId)
|
2021-11-19 11:13:41 -08:00
|
|
|
url := fmt.Sprintf("%vcoins/%v/contract/%v", baseUrl, platform, address)
|
2021-11-19 11:09:39 -08:00
|
|
|
req, reqErr := http.NewRequest("GET", url, nil)
|
|
|
|
if reqErr != nil {
|
|
|
|
log.Fatalf("failed contract request, err: %v\n", reqErr)
|
|
|
|
}
|
2021-11-19 11:13:41 -08:00
|
|
|
if cgApiKey != "" {
|
|
|
|
req.Header.Set("X-Cg-Pro-Api-Key", cgApiKey)
|
|
|
|
}
|
2021-11-19 11:09:39 -08:00
|
|
|
|
|
|
|
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 {
|
2022-03-02 14:37:05 -08:00
|
|
|
log.Printf("fetchCoinGeckoCoinFromContract failed parsing body. err %v\n", parseErr)
|
2021-11-19 11:09:39 -08:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2021-11-16 10:20:20 -08:00
|
|
|
func fetchCoinGeckoPrice(coinId string, timestamp time.Time) (float64, error) {
|
2021-11-19 11:09:39 -08:00
|
|
|
hourAgo := time.Now().Add(-time.Duration(1) * time.Hour)
|
|
|
|
withinLastHour := timestamp.After(hourAgo)
|
2022-03-02 14:37:05 -08:00
|
|
|
start, end := rangeFromTime(timestamp, 12)
|
2021-11-19 11:13:41 -08:00
|
|
|
|
|
|
|
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())
|
2021-11-16 10:20:20 -08:00
|
|
|
req, reqErr := http.NewRequest("GET", url, nil)
|
|
|
|
if reqErr != nil {
|
|
|
|
log.Fatalf("failed coins request, err: %v\n", reqErr)
|
|
|
|
}
|
2021-11-19 11:13:41 -08:00
|
|
|
if cgApiKey != "" {
|
|
|
|
req.Header.Set("X-Cg-Pro-Api-Key", cgApiKey)
|
|
|
|
}
|
2021-11-16 10:20:20 -08:00
|
|
|
|
|
|
|
res, resErr := http.DefaultClient.Do(req)
|
|
|
|
if resErr != nil {
|
|
|
|
log.Fatalf("failed get coins response, err: %v\n", resErr)
|
|
|
|
}
|
2022-04-28 05:07:46 -07:00
|
|
|
if res.StatusCode >= 400 {
|
|
|
|
log.Fatal("failed to get CoinGecko prices. Status", res.Status)
|
|
|
|
}
|
2021-11-16 10:20:20 -08:00
|
|
|
|
|
|
|
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 {
|
2022-03-02 14:37:05 -08:00
|
|
|
log.Printf("fetchCoinGeckoPrice failed parsing body. err %v\n", parseErr)
|
2021-11-19 11:09:39 -08:00
|
|
|
var errRes CoinGeckoErrorRes
|
|
|
|
if err := json.Unmarshal(body, &errRes); err == nil {
|
|
|
|
log.Println("Failed calling CoinGecko, got err", errRes.Error)
|
|
|
|
}
|
2021-11-16 10:20:20 -08:00
|
|
|
}
|
|
|
|
if len(parsed.Prices) >= 1 {
|
2021-11-19 11:09:39 -08:00
|
|
|
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]
|
2021-12-12 21:51:10 -08:00
|
|
|
log.Printf("found a price of $%f for %v!\n", price, coinId)
|
2021-11-16 10:20:20 -08:00
|
|
|
return price, nil
|
|
|
|
}
|
2021-12-12 21:51:10 -08:00
|
|
|
log.Println("no price found in coinGecko for", coinId)
|
2021-11-16 10:20:20 -08:00
|
|
|
return 0, fmt.Errorf("no price found for %v", coinId)
|
|
|
|
}
|
|
|
|
|
2022-02-23 06:01:47 -08:00
|
|
|
type Price struct {
|
|
|
|
USD float64 `json:"usd"`
|
|
|
|
}
|
|
|
|
type CoinGeckoCoinPrices map[string]Price
|
|
|
|
|
|
|
|
// takes a list of CoinGeckoCoinIds, returns a map of { coinId: price }.
|
|
|
|
func fetchCoinGeckoPrices(coinIds []string) (map[string]float64, error) {
|
|
|
|
baseUrl := cgBaseUrl
|
|
|
|
cgApiKey := os.Getenv("COINGECKO_API_KEY")
|
|
|
|
if cgApiKey != "" {
|
|
|
|
baseUrl = cgProBaseUrl
|
|
|
|
}
|
|
|
|
url := fmt.Sprintf("%vsimple/price?ids=%v&vs_currencies=usd", baseUrl, strings.Join(coinIds, ","))
|
|
|
|
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)
|
|
|
|
}
|
2022-04-28 05:07:46 -07:00
|
|
|
if res.StatusCode >= 400 {
|
|
|
|
log.Fatal("failed to get CoinGecko prices. Status", res.Status)
|
|
|
|
}
|
2022-02-23 06:01:47 -08:00
|
|
|
|
|
|
|
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 CoinGeckoCoinPrices
|
|
|
|
|
|
|
|
parseErr := json.Unmarshal(body, &parsed)
|
|
|
|
if parseErr != nil {
|
|
|
|
log.Printf("fetchCoinGeckoPrice 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)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
priceMap := map[string]float64{}
|
|
|
|
for coinId, price := range parsed {
|
|
|
|
price := price.USD
|
|
|
|
priceMap[coinId] = price
|
|
|
|
|
|
|
|
}
|
|
|
|
return priceMap, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// takes a list of CoinGeckoCoinIds, returns a map of { coinId: price }.
|
|
|
|
// makes batches of requests to CoinGecko.
|
|
|
|
func fetchTokenPrices(ctx context.Context, coinIds []string) map[string]float64 {
|
|
|
|
allPrices := map[string]float64{}
|
|
|
|
|
|
|
|
// Split the list into batches, otherwise the request could be too large
|
|
|
|
batch := 100
|
|
|
|
|
|
|
|
for i := 0; i < len(coinIds); i += batch {
|
|
|
|
j := i + batch
|
|
|
|
if j > len(coinIds) {
|
|
|
|
j = len(coinIds)
|
|
|
|
}
|
|
|
|
|
|
|
|
prices, err := fetchCoinGeckoPrices(coinIds[i:j])
|
|
|
|
if err != nil {
|
|
|
|
log.Fatalf("failed to get price for coinIds. err %v", err)
|
|
|
|
}
|
|
|
|
for coinId, price := range prices {
|
|
|
|
allPrices[coinId] = price
|
|
|
|
}
|
|
|
|
|
|
|
|
// CoinGecko rate limit is low (5/second), be very cautious about bursty requests
|
|
|
|
time.Sleep(time.Millisecond * 200)
|
|
|
|
}
|
|
|
|
|
|
|
|
return allPrices
|
|
|
|
}
|
|
|
|
|
2021-11-16 10:20:20 -08:00
|
|
|
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 {
|
2022-03-17 15:01:35 -07:00
|
|
|
defer timeTrack(time.Now(), "fetchSolanaTokenList")
|
2021-11-16 10:20:20 -08:00
|
|
|
|
|
|
|
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 {
|
2022-03-02 14:37:05 -08:00
|
|
|
log.Printf("fetchSolanaTokenList failed parsing body. err %v\n", parseErr)
|
2021-11-16 10:20:20 -08:00
|
|
|
}
|
|
|
|
var solTokens = map[string]SolanaToken{}
|
|
|
|
for _, token := range parsed.Tokens {
|
|
|
|
if _, ok := solTokens[token.Address]; !ok {
|
|
|
|
solTokens[token.Address] = token
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return solTokens
|
|
|
|
}
|
2021-12-13 10:33:23 -08:00
|
|
|
|
|
|
|
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 {
|
2022-03-02 14:37:05 -08:00
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second*15)
|
|
|
|
defer cancel()
|
|
|
|
|
2021-12-13 10:33:23 -08:00
|
|
|
baseUrl := solanaBeachPublicBaseURL
|
|
|
|
|
|
|
|
sbApiKey := os.Getenv("SOLANABEACH_API_KEY")
|
|
|
|
if sbApiKey != "" {
|
|
|
|
baseUrl = solanaBeachPrivateBaseURL
|
|
|
|
}
|
|
|
|
|
|
|
|
url := fmt.Sprintf("%vaccount/%v", baseUrl, account)
|
2022-03-02 14:37:05 -08:00
|
|
|
req, reqErr := http.NewRequestWithContext(ctx, "GET", url, nil)
|
2021-12-13 10:33:23 -08:00
|
|
|
if reqErr != nil {
|
2022-03-02 14:37:05 -08:00
|
|
|
log.Printf("failed solanabeach request, err: %v", reqErr)
|
|
|
|
return ""
|
2021-12-13 10:33:23 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
if sbApiKey != "" {
|
|
|
|
req.Header.Set("Authorization", fmt.Sprintf("Bearer %v", sbApiKey))
|
|
|
|
}
|
|
|
|
|
|
|
|
res, resErr := http.DefaultClient.Do(req)
|
|
|
|
if resErr != nil {
|
2022-03-02 14:37:05 -08:00
|
|
|
log.Printf("failed get solana beach account response, err: %v", resErr)
|
|
|
|
return ""
|
2021-12-13 10:33:23 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
defer res.Body.Close()
|
|
|
|
body, bodyErr := ioutil.ReadAll(res.Body)
|
|
|
|
if bodyErr != nil {
|
2022-03-02 14:37:05 -08:00
|
|
|
log.Printf("failed decoding solana beach account body, err: %v", bodyErr)
|
|
|
|
return ""
|
2021-12-13 10:33:23 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
var parsed SolanaBeachAccountResponse
|
|
|
|
|
|
|
|
parseErr := json.Unmarshal(body, &parsed)
|
|
|
|
if parseErr != nil {
|
2022-03-02 14:37:05 -08:00
|
|
|
log.Printf("fetchSolanaAccountOwner failed parsing body. err %v\n", parseErr)
|
|
|
|
return ""
|
2021-12-13 10:33:23 -08:00
|
|
|
}
|
|
|
|
address := parsed.Value.Extended.Owner.Address
|
2022-03-02 14:37:05 -08:00
|
|
|
if address == "" {
|
|
|
|
log.Println("failed to find owner address for Solana account", account)
|
|
|
|
}
|
2021-12-13 10:33:23 -08:00
|
|
|
return address
|
|
|
|
}
|