external-data.go 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343
  1. package p
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. "io/ioutil"
  6. "log"
  7. "os"
  8. "strings"
  9. "time"
  10. "net/http"
  11. "github.com/certusone/wormhole/node/pkg/vaa"
  12. )
  13. const cgBaseUrl = "https://api.coingecko.com/api/v3/"
  14. const cgProBaseUrl = "https://pro-api.coingecko.com/api/v3/"
  15. type CoinGeckoCoin struct {
  16. Id string `json:"id"`
  17. Symbol string `json:"symbol"`
  18. Name string `json:"name"`
  19. }
  20. type CoinGeckoCoins []CoinGeckoCoin
  21. type CoinGeckoMarket [2]float64
  22. type CoinGeckoMarketRes struct {
  23. Prices []CoinGeckoMarket `json:"prices"`
  24. }
  25. type CoinGeckoErrorRes struct {
  26. Error string `json:"error"`
  27. }
  28. func fetchCoinGeckoCoins() map[string][]CoinGeckoCoin {
  29. baseUrl := cgBaseUrl
  30. cgApiKey := os.Getenv("COINGECKO_API_KEY")
  31. if cgApiKey != "" {
  32. baseUrl = cgProBaseUrl
  33. }
  34. url := fmt.Sprintf("%vcoins/list", baseUrl)
  35. req, reqErr := http.NewRequest("GET", url, nil)
  36. if reqErr != nil {
  37. log.Fatalf("failed coins request, err: %v", reqErr)
  38. }
  39. if cgApiKey != "" {
  40. req.Header.Set("X-Cg-Pro-Api-Key", cgApiKey)
  41. }
  42. res, resErr := http.DefaultClient.Do(req)
  43. if resErr != nil {
  44. log.Fatalf("failed get coins response, err: %v", resErr)
  45. }
  46. defer res.Body.Close()
  47. body, bodyErr := ioutil.ReadAll(res.Body)
  48. if bodyErr != nil {
  49. log.Fatalf("failed decoding coins body, err: %v", bodyErr)
  50. }
  51. var parsed []CoinGeckoCoin
  52. parseErr := json.Unmarshal(body, &parsed)
  53. if parseErr != nil {
  54. log.Printf("failed parsing body. err %v\n", parseErr)
  55. }
  56. var geckoCoins = map[string][]CoinGeckoCoin{}
  57. for _, coin := range parsed {
  58. symbol := strings.ToLower(coin.Symbol)
  59. geckoCoins[symbol] = append(geckoCoins[symbol], coin)
  60. }
  61. return geckoCoins
  62. }
  63. func chainIdToCoinGeckoPlatform(chain vaa.ChainID) string {
  64. switch chain {
  65. case vaa.ChainIDSolana:
  66. return "solana"
  67. case vaa.ChainIDEthereum:
  68. return "ethereum"
  69. case vaa.ChainIDTerra:
  70. return "terra"
  71. case vaa.ChainIDBSC:
  72. return "binance-smart-chain"
  73. case vaa.ChainIDPolygon:
  74. return "polygon-pos"
  75. }
  76. return ""
  77. }
  78. func fetchCoinGeckoCoinFromContract(chainId vaa.ChainID, address string) CoinGeckoCoin {
  79. baseUrl := cgBaseUrl
  80. cgApiKey := os.Getenv("COINGECKO_API_KEY")
  81. if cgApiKey != "" {
  82. baseUrl = cgProBaseUrl
  83. }
  84. platform := chainIdToCoinGeckoPlatform(chainId)
  85. url := fmt.Sprintf("%vcoins/%v/contract/%v", baseUrl, platform, address)
  86. req, reqErr := http.NewRequest("GET", url, nil)
  87. if reqErr != nil {
  88. log.Fatalf("failed contract request, err: %v\n", reqErr)
  89. }
  90. if cgApiKey != "" {
  91. req.Header.Set("X-Cg-Pro-Api-Key", cgApiKey)
  92. }
  93. res, resErr := http.DefaultClient.Do(req)
  94. if resErr != nil {
  95. log.Fatalf("failed get contract response, err: %v\n", resErr)
  96. }
  97. defer res.Body.Close()
  98. body, bodyErr := ioutil.ReadAll(res.Body)
  99. if bodyErr != nil {
  100. log.Fatalf("failed decoding contract body, err: %v\n", bodyErr)
  101. }
  102. var parsed CoinGeckoCoin
  103. parseErr := json.Unmarshal(body, &parsed)
  104. if parseErr != nil {
  105. log.Printf("failed parsing body. err %v\n", parseErr)
  106. var errRes CoinGeckoErrorRes
  107. if err := json.Unmarshal(body, &errRes); err == nil {
  108. if errRes.Error == "Could not find coin with the given id" {
  109. log.Printf("Could not find CoinGecko coin by contract address, for chain %v, address, %v\n", chainId, address)
  110. } else {
  111. log.Println("Failed calling CoinGecko, got err", errRes.Error)
  112. }
  113. }
  114. }
  115. return parsed
  116. }
  117. func fetchCoinGeckoCoinId(chainId vaa.ChainID, address, symbol, name string) (coinId, foundSymbol, foundName string) {
  118. // try coingecko, return if good
  119. // if coingecko does not work, try chain-specific options
  120. // initialize strings that will be returned if we find a symbol/name
  121. // when looking up this token by contract address
  122. newSymbol := ""
  123. newName := ""
  124. if symbol == "" && chainId == vaa.ChainIDSolana {
  125. // try to lookup the symbol in solana token list, from the address
  126. if token, ok := solanaTokens[address]; ok {
  127. symbol = token.Symbol
  128. name = token.Name
  129. newSymbol = token.Symbol
  130. newName = token.Name
  131. }
  132. }
  133. if _, ok := coinGeckoCoins[strings.ToLower(symbol)]; ok {
  134. tokens := coinGeckoCoins[strings.ToLower(symbol)]
  135. if len(tokens) == 1 {
  136. // only one match found for this symbol
  137. return tokens[0].Id, newSymbol, newName
  138. }
  139. for _, token := range tokens {
  140. if token.Name == name {
  141. // found token by name match
  142. return token.Id, newSymbol, newName
  143. }
  144. if strings.Contains(strings.ToLower(strings.ReplaceAll(name, " ", "")), strings.ReplaceAll(token.Id, "-", "")) {
  145. // found token by id match
  146. log.Println("found token by symbol and name match", name)
  147. return token.Id, newSymbol, newName
  148. }
  149. }
  150. // more than one symbol with this name, let contract lookup try
  151. }
  152. coin := fetchCoinGeckoCoinFromContract(chainId, address)
  153. if coin.Id != "" {
  154. return coin.Id, newSymbol, newName
  155. }
  156. // could not find a CoinGecko coin
  157. return "", newSymbol, newName
  158. }
  159. func fetchCoinGeckoPrice(coinId string, timestamp time.Time) (float64, error) {
  160. hourAgo := time.Now().Add(-time.Duration(1) * time.Hour)
  161. withinLastHour := timestamp.After(hourAgo)
  162. start, end := rangeFromTime(timestamp, 4)
  163. baseUrl := cgBaseUrl
  164. cgApiKey := os.Getenv("COINGECKO_API_KEY")
  165. if cgApiKey != "" {
  166. baseUrl = cgProBaseUrl
  167. }
  168. url := fmt.Sprintf("%vcoins/%v/market_chart/range?vs_currency=usd&from=%v&to=%v", baseUrl, coinId, start.Unix(), end.Unix())
  169. req, reqErr := http.NewRequest("GET", url, nil)
  170. if reqErr != nil {
  171. log.Fatalf("failed coins request, err: %v\n", reqErr)
  172. }
  173. if cgApiKey != "" {
  174. req.Header.Set("X-Cg-Pro-Api-Key", cgApiKey)
  175. }
  176. res, resErr := http.DefaultClient.Do(req)
  177. if resErr != nil {
  178. log.Fatalf("failed get coins response, err: %v\n", resErr)
  179. }
  180. defer res.Body.Close()
  181. body, bodyErr := ioutil.ReadAll(res.Body)
  182. if bodyErr != nil {
  183. log.Fatalf("failed decoding coins body, err: %v\n", bodyErr)
  184. }
  185. var parsed CoinGeckoMarketRes
  186. parseErr := json.Unmarshal(body, &parsed)
  187. if parseErr != nil {
  188. log.Printf("failed parsing body. err %v\n", parseErr)
  189. var errRes CoinGeckoErrorRes
  190. if err := json.Unmarshal(body, &errRes); err == nil {
  191. log.Println("Failed calling CoinGecko, got err", errRes.Error)
  192. }
  193. }
  194. if len(parsed.Prices) >= 1 {
  195. var priceIndex int
  196. if withinLastHour {
  197. // use the last price in the list, latest price
  198. priceIndex = len(parsed.Prices) - 1
  199. } else {
  200. // use a price from the middle of the list, as that should be
  201. // closest to the timestamp.
  202. numPrices := len(parsed.Prices)
  203. priceIndex = numPrices / 2
  204. }
  205. price := parsed.Prices[priceIndex][1]
  206. log.Printf("found a price of $%f for %v!\n", price, coinId)
  207. return price, nil
  208. }
  209. log.Println("no price found in coinGecko for", coinId)
  210. return 0, fmt.Errorf("no price found for %v", coinId)
  211. }
  212. const solanaTokenListURL = "https://raw.githubusercontent.com/solana-labs/token-list/main/src/tokens/solana.tokenlist.json"
  213. type SolanaToken struct {
  214. Address string `json:"address"`
  215. Symbol string `json:"symbol"`
  216. Name string `json:"name"`
  217. Decimals int `json:"decimals"`
  218. }
  219. type SolanaTokenListRes struct {
  220. Tokens []SolanaToken `json:"tokens"`
  221. }
  222. func fetchSolanaTokenList() map[string]SolanaToken {
  223. req, reqErr := http.NewRequest("GET", solanaTokenListURL, nil)
  224. if reqErr != nil {
  225. log.Fatalf("failed solana token list request, err: %v", reqErr)
  226. }
  227. res, resErr := http.DefaultClient.Do(req)
  228. if resErr != nil {
  229. log.Fatalf("failed get solana token list response, err: %v", resErr)
  230. }
  231. defer res.Body.Close()
  232. body, bodyErr := ioutil.ReadAll(res.Body)
  233. if bodyErr != nil {
  234. log.Fatalf("failed decoding solana token list body, err: %v", bodyErr)
  235. }
  236. var parsed SolanaTokenListRes
  237. parseErr := json.Unmarshal(body, &parsed)
  238. if parseErr != nil {
  239. log.Printf("failed parsing body. err %v\n", parseErr)
  240. }
  241. var solTokens = map[string]SolanaToken{}
  242. for _, token := range parsed.Tokens {
  243. if _, ok := solTokens[token.Address]; !ok {
  244. solTokens[token.Address] = token
  245. }
  246. }
  247. return solTokens
  248. }
  249. const solanaBeachPublicBaseURL = "https://prod-api.solana.surf/v1/"
  250. const solanaBeachPrivateBaseURL = "https://api.solanabeach.io/v1/"
  251. type SolanaBeachAccountOwner struct {
  252. Owner SolanaBeachAccountOwnerAddress `json:"owner"`
  253. }
  254. type SolanaBeachAccountOwnerAddress struct {
  255. Address string `json:"address"`
  256. }
  257. type SolanaBeachAccountResponse struct {
  258. Value struct {
  259. Extended struct {
  260. SolanaBeachAccountOwner
  261. } `json:"extended"`
  262. } `json:"value"`
  263. }
  264. func fetchSolanaAccountOwner(account string) string {
  265. baseUrl := solanaBeachPublicBaseURL
  266. sbApiKey := os.Getenv("SOLANABEACH_API_KEY")
  267. if sbApiKey != "" {
  268. baseUrl = solanaBeachPrivateBaseURL
  269. }
  270. url := fmt.Sprintf("%vaccount/%v", baseUrl, account)
  271. req, reqErr := http.NewRequest("GET", url, nil)
  272. if reqErr != nil {
  273. log.Fatalf("failed solanabeach request, err: %v", reqErr)
  274. }
  275. if sbApiKey != "" {
  276. req.Header.Set("Authorization", fmt.Sprintf("Bearer %v", sbApiKey))
  277. }
  278. res, resErr := http.DefaultClient.Do(req)
  279. if resErr != nil {
  280. log.Fatalf("failed get solana beach account response, err: %v", resErr)
  281. }
  282. defer res.Body.Close()
  283. body, bodyErr := ioutil.ReadAll(res.Body)
  284. if bodyErr != nil {
  285. log.Fatalf("failed decoding solana beach account body, err: %v", bodyErr)
  286. }
  287. var parsed SolanaBeachAccountResponse
  288. parseErr := json.Unmarshal(body, &parsed)
  289. if parseErr != nil {
  290. log.Printf("failed parsing body. err %v\n", parseErr)
  291. }
  292. address := parsed.Value.Extended.Owner.Address
  293. log.Println("got owner address from Solana Beach! ", address)
  294. return address
  295. }