github.com/diadata-org/diadata@v1.4.593/pkg/http/restServer/diaApi/diaApi.go (about) 1 package diaApi 2 3 import ( 4 "errors" 5 "fmt" 6 "math" 7 "net/http" 8 "sort" 9 "strconv" 10 "strings" 11 "time" 12 13 "github.com/diadata-org/diadata/pkg/dia" 14 "github.com/diadata-org/diadata/pkg/dia/helpers/gqlclient" 15 "github.com/diadata-org/diadata/pkg/http/restApi" 16 models "github.com/diadata-org/diadata/pkg/model" 17 "github.com/diadata-org/diadata/pkg/utils" 18 "github.com/ethereum/go-ethereum/common" 19 "github.com/gin-gonic/gin" 20 "github.com/go-redis/redis" 21 log "github.com/sirupsen/logrus" 22 ) 23 24 var ( 25 DECIMALS_CACHE = make(map[dia.Asset]uint8) 26 ASSET_CACHE = make(map[string]dia.Asset) 27 QUOTATION_CACHE = make(map[string]*models.AssetQuotation) 28 BLOCKCHAINS = make(map[string]dia.BlockChain) 29 ASSETQUOTATION_LOOKBACK_HOURS = 24 * 7 30 ) 31 32 type Env struct { 33 DataStore models.Datastore 34 RelDB models.RelDB 35 signer *utils.AssetQuotationSigner 36 } 37 38 func init() { 39 relDB, err := models.NewRelDataStore() 40 if err != nil { 41 log.Errorln("Error connecting to asset DB: ", err) 42 return 43 } 44 chains, err := relDB.GetAllBlockchains(false) 45 if err != nil { 46 log.Fatal("get all chains: ", err) 47 } 48 for _, chain := range chains { 49 BLOCKCHAINS[chain.Name] = chain 50 } 51 } 52 53 func NewEnv(ds models.Datastore, rdb models.RelDB, signer *utils.AssetQuotationSigner) *Env { 54 return &Env{DataStore: ds, RelDB: rdb, signer: signer} 55 } 56 57 // GetAssetQuotation returns quotation of asset with highest market cap among 58 // all assets with symbol ticker @symbol. 59 func (env *Env) GetAssetQuotation(c *gin.Context) { 60 if !validateInputParams(c) { 61 return 62 } 63 64 blockchain := c.Param("blockchain") 65 address := normalizeAddress(c.Param("address"), blockchain) 66 67 var ( 68 err error 69 asset dia.Asset 70 quotationExtended models.AssetQuotationFull 71 ) 72 73 // Time for quotation is now by default. 74 timestampInt, err := strconv.ParseInt(c.DefaultQuery("timestamp", strconv.Itoa(int(time.Now().Unix()))), 10, 64) 75 if err != nil { 76 restApi.SendError(c, http.StatusNotFound, errors.New("could not parse Unix timestamp")) 77 return 78 } 79 timestamp := time.Unix(timestampInt, 0) 80 81 // An asset is uniquely defined by blockchain and address. 82 asset, err = env.RelDB.GetAsset(address, blockchain) 83 if err != nil { 84 restApi.SendError(c, http.StatusNotFound, err) 85 return 86 } 87 88 // Get quotation for asset. 89 quotation, err := env.DataStore.GetAssetQuotation(asset, dia.CRYPTO_ZERO_UNIX_TIME, timestamp) 90 if err != nil { 91 restApi.SendError(c, http.StatusNotFound, err) 92 return 93 } 94 95 quotationYesterday, err := env.DataStore.GetAssetQuotation(asset, dia.CRYPTO_ZERO_UNIX_TIME, timestamp.AddDate(0, 0, -1)) 96 if err != nil { 97 log.Warn("get quotation yesterday: ", err) 98 } else { 99 quotationExtended.PriceYesterday = quotationYesterday.Price 100 } 101 102 volumeYesterday, err := env.DataStore.Get24HoursAssetVolume(asset) 103 if err != nil { 104 log.Warn("get volume yesterday: ", err) 105 } else { 106 quotationExtended.VolumeYesterdayUSD = *volumeYesterday 107 } 108 109 // Appropriate formatting. 110 quotationExtended.Symbol = quotation.Asset.Symbol 111 quotationExtended.Name = quotation.Asset.Name 112 quotationExtended.Address = quotation.Asset.Address 113 quotationExtended.Blockchain = quotation.Asset.Blockchain 114 quotationExtended.Price = quotation.Price 115 quotationExtended.Time = quotation.Time 116 quotationExtended.Source = quotation.Source 117 118 signedData, err := env.signer.Sign(quotation.Asset.Symbol, quotation.Asset.Address, quotation.Asset.Blockchain, quotation.Price, quotationExtended.Time) 119 if err != nil { 120 log.Warn("error signing data: ", err) 121 } 122 quotationExtended.Signature = signedData 123 124 c.JSON(http.StatusOK, quotationExtended) 125 126 } 127 128 // GetQuotation returns quotation of asset with highest market cap among 129 // all assets with symbol ticker @symbol. 130 func (env *Env) GetQuotation(c *gin.Context) { 131 if !validateInputParams(c) { 132 return 133 } 134 135 symbol := c.Param("symbol") 136 137 timestamp := time.Now() 138 var quotationExtended models.AssetQuotationFull 139 // Fetch underlying assets for symbol 140 assets, err := env.RelDB.GetTopAssetByVolume(symbol) 141 if err != nil { 142 restApi.SendError(c, http.StatusNotFound, err) 143 return 144 } 145 if len(assets) == 0 { 146 restApi.SendError(c, http.StatusNotFound, errors.New("no quotation available")) 147 return 148 } 149 topAsset := assets[0] 150 quotation, err := env.DataStore.GetAssetQuotation(topAsset, dia.CRYPTO_ZERO_UNIX_TIME, timestamp) 151 if err != nil { 152 restApi.SendError(c, http.StatusNotFound, errors.New("no quotation available")) 153 return 154 } 155 quotationYesterday, err := env.DataStore.GetAssetQuotation(topAsset, dia.CRYPTO_ZERO_UNIX_TIME, timestamp.AddDate(0, 0, -1)) 156 if err != nil { 157 log.Warn("get quotation yesterday: ", err) 158 } else { 159 quotationExtended.PriceYesterday = quotationYesterday.Price 160 } 161 volumeYesterday, err := env.DataStore.Get24HoursAssetVolume(topAsset) 162 if err != nil { 163 log.Warn("get volume yesterday: ", err) 164 } else { 165 quotationExtended.VolumeYesterdayUSD = *volumeYesterday 166 } 167 quotationExtended.Symbol = quotation.Asset.Symbol 168 quotationExtended.Name = quotation.Asset.Name 169 quotationExtended.Address = quotation.Asset.Address 170 quotationExtended.Blockchain = quotation.Asset.Blockchain 171 quotationExtended.Price = quotation.Price 172 quotationExtended.Time = quotation.Time 173 quotationExtended.Source = quotation.Source 174 175 c.JSON(http.StatusOK, quotationExtended) 176 } 177 178 func (env *Env) GetAssetMap(c *gin.Context) { 179 if !validateInputParams(c) { 180 return 181 } 182 183 blockchain := c.Param("blockchain") 184 address := normalizeAddress(c.Param("address"), blockchain) 185 186 timestamp := time.Now() 187 var quotations []models.AssetQuotationFull 188 // Fetch underlying assets for symbol 189 asset, err := env.RelDB.GetAsset(address, blockchain) 190 if err != nil { 191 restApi.SendError(c, http.StatusNotFound, err) 192 return 193 } 194 195 // get assetid 196 assetid, err := env.RelDB.GetAssetID(asset) 197 if err != nil { 198 restApi.SendError(c, http.StatusNotFound, err) 199 return 200 } 201 202 // get groupId 203 group_id, err := env.RelDB.GetAssetMap(assetid) 204 if err != nil { 205 restApi.SendError(c, http.StatusNotFound, err) 206 return 207 } 208 209 assets, err := env.RelDB.GetAssetByGroupID(group_id) 210 if err != nil || len(assets) == 0 { 211 restApi.SendError(c, http.StatusNotFound, errors.New("no quotation available")) 212 return 213 } 214 215 for _, topAsset := range assets { 216 var quotationExtended models.AssetQuotationFull 217 218 quotation, err := env.DataStore.GetAssetQuotation(topAsset, dia.CRYPTO_ZERO_UNIX_TIME, timestamp) 219 if err != nil { 220 log.Warn("get quotation: ", err) 221 } 222 quotationYesterday, err := env.DataStore.GetAssetQuotation(topAsset, dia.CRYPTO_ZERO_UNIX_TIME, timestamp.AddDate(0, 0, -1)) 223 if err != nil { 224 log.Warn("get quotation yesterday: ", err) 225 } else { 226 quotationExtended.PriceYesterday = quotationYesterday.Price 227 } 228 volumeYesterday, err := env.RelDB.GetLastAssetVolume24H(topAsset) 229 if err != nil { 230 log.Warn("get volume yesterday: ", err) 231 } else { 232 quotationExtended.VolumeYesterdayUSD = volumeYesterday 233 } 234 quotationExtended.Symbol = topAsset.Symbol 235 quotationExtended.Name = topAsset.Name 236 quotationExtended.Address = topAsset.Address 237 quotationExtended.Blockchain = topAsset.Blockchain 238 quotationExtended.Price = quotation.Price 239 quotationExtended.Time = quotation.Time 240 quotationExtended.Source = quotation.Source 241 quotations = append(quotations, quotationExtended) 242 } 243 244 c.JSON(http.StatusOK, quotations) 245 } 246 247 // GetSupply returns latest supply of token with @symbol 248 func (env *Env) GetSupply(c *gin.Context) { 249 if !validateInputParams(c) { 250 return 251 } 252 253 symbol := c.Param("symbol") 254 255 s, err := env.DataStore.GetLatestSupply(symbol, &env.RelDB) 256 if err != nil { 257 if errors.Is(err, redis.Nil) { 258 restApi.SendError(c, http.StatusNotFound, err) 259 } else { 260 restApi.SendError(c, http.StatusInternalServerError, err) 261 } 262 } else { 263 c.JSON(http.StatusOK, s) 264 } 265 } 266 267 // GetSupply returns latest supply of token with @symbol 268 func (env *Env) GetAssetSupply(c *gin.Context) { 269 if !validateInputParams(c) { 270 return 271 } 272 273 blockchain := c.Param("blockchain") 274 address := normalizeAddress(c.Param("address"), blockchain) 275 276 starttime, endtime, err := utils.MakeTimerange(c.Query("starttime"), c.Query("endtime"), time.Duration(24*time.Hour)) 277 if err != nil { 278 restApi.SendError(c, http.StatusInternalServerError, fmt.Errorf("parse time range")) 279 return 280 } 281 282 if ok := utils.ValidTimeRange(starttime, endtime, time.Duration(30*24*time.Hour)); !ok { 283 restApi.SendError(c, http.StatusInternalServerError, fmt.Errorf("time-range too big. max duration is %v", 30*24*time.Hour)) 284 return 285 } 286 287 values, err := env.DataStore.GetSupplyInflux(dia.Asset{Address: address, Blockchain: blockchain}, starttime, endtime) 288 if err != nil { 289 if errors.Is(err, redis.Nil) { 290 restApi.SendError(c, http.StatusNotFound, err) 291 return 292 } else { 293 restApi.SendError(c, http.StatusInternalServerError, err) 294 return 295 } 296 } 297 298 // Fetch decimals from local cache implementation. 299 for i := range values { 300 values[i].Asset.Decimals = env.getDecimalsFromCache(DECIMALS_CACHE, values[i].Asset) 301 } 302 303 if len(values) == 1 { 304 c.JSON(http.StatusOK, values[0]) 305 } else { 306 c.JSON(http.StatusOK, values) 307 } 308 309 } 310 311 // GetSupplies returns a time range of supplies of token with @symbol 312 func (env *Env) GetSupplies(c *gin.Context) { 313 if !validateInputParams(c) { 314 return 315 } 316 317 symbol := c.Param("symbol") 318 starttimeStr := c.DefaultQuery("starttime", "noRange") 319 endtimeStr := c.Query("endtime") 320 321 var starttime, endtime time.Time 322 323 if starttimeStr == "noRange" || endtimeStr == "" { 324 endtime = time.Now() 325 starttime = endtime.AddDate(0, 0, -30) 326 } else { 327 starttimeInt, err := strconv.ParseInt(starttimeStr, 10, 64) 328 if err != nil { 329 restApi.SendError(c, http.StatusInternalServerError, err) 330 return 331 } 332 starttime = time.Unix(starttimeInt, 0) 333 endtimeInt, err := strconv.ParseInt(endtimeStr, 10, 64) 334 if err != nil { 335 restApi.SendError(c, http.StatusInternalServerError, err) 336 return 337 } 338 endtime = time.Unix(endtimeInt, 0) 339 } 340 if ok := utils.ValidTimeRange(starttime, endtime, time.Duration(30*24*time.Hour)); !ok { 341 restApi.SendError(c, http.StatusInternalServerError, fmt.Errorf("time-range too big. max duration is %v", 30*24*time.Hour)) 342 return 343 } 344 345 s, err := env.DataStore.GetSupply(symbol, starttime, endtime, &env.RelDB) 346 if len(s) == 0 { 347 c.JSON(http.StatusOK, make([]string, 0)) 348 return 349 } 350 if err != nil { 351 restApi.SendError(c, http.StatusInternalServerError, err) 352 return 353 } 354 c.JSON(http.StatusOK, s) 355 } 356 357 func (env *Env) GetDiaTotalSupply(c *gin.Context) { 358 q, err := env.DataStore.GetDiaTotalSupply() 359 if err != nil { 360 if errors.Is(err, redis.Nil) { 361 restApi.SendError(c, http.StatusNotFound, err) 362 } else { 363 restApi.SendError(c, http.StatusInternalServerError, err) 364 } 365 } else { 366 c.JSON(http.StatusOK, q) 367 } 368 } 369 370 func (env *Env) GetDiaCirculatingSupply(c *gin.Context) { 371 q, err := env.DataStore.GetDiaCirculatingSupply() 372 if err != nil { 373 if errors.Is(err, redis.Nil) { 374 restApi.SendError(c, http.StatusNotFound, err) 375 } else { 376 restApi.SendError(c, http.StatusInternalServerError, err) 377 } 378 } else { 379 c.JSON(http.StatusOK, q) 380 } 381 } 382 383 // Get24hVolume if no times are set use the last 24h 384 func (env *Env) Get24hVolume(c *gin.Context) { 385 if !validateInputParams(c) { 386 return 387 } 388 389 v, err := env.DataStore.Get24HoursExchangeVolume(c.Param("exchange")) 390 if err != nil { 391 restApi.SendError(c, http.StatusInternalServerError, err) 392 return 393 } 394 c.JSON(http.StatusOK, *v) 395 } 396 397 // GetExchanges is the delegate method for fetching all exchanges available in Postgres. 398 func (env *Env) GetExchanges(c *gin.Context) { 399 400 starttime, endtime, err := utils.MakeTimerange(c.Query("starttime"), c.Query("endtime"), time.Duration(24*time.Hour)) 401 if err != nil { 402 restApi.SendError(c, http.StatusInternalServerError, fmt.Errorf("parse time range")) 403 return 404 } 405 if starttime.Before(endtime.AddDate(0, 0, -30)) { 406 restApi.SendError(c, http.StatusInternalServerError, errors.New("time range is limited to 30 days")) 407 return 408 } 409 410 type exchangeReturn struct { 411 Name string 412 Volume24h float64 413 Trades int64 414 Pairs int 415 Type string 416 Blockchain string 417 ScraperActive bool 418 } 419 var exchangereturns []exchangeReturn 420 exchanges, err := env.RelDB.GetAllExchanges() 421 if len(exchanges) == 0 || err != nil { 422 restApi.SendError(c, http.StatusInternalServerError, nil) 423 } 424 425 dexPoolCountMap, err := env.RelDB.GetAllDEXPoolsCount() 426 if err != nil { 427 restApi.SendError(c, http.StatusInternalServerError, nil) 428 } 429 430 for _, exchange := range exchanges { 431 var numPairs int 432 433 vol, err := env.DataStore.GetVolumeInflux(dia.Asset{}, exchange.Name, starttime, endtime) 434 if err != nil { 435 restApi.SendError(c, http.StatusInternalServerError, err) 436 return 437 } 438 439 numTrades, err := env.DataStore.GetNumTrades(exchange.Name, "", "", starttime, endtime) 440 if err != nil { 441 restApi.SendError(c, http.StatusInternalServerError, err) 442 return 443 } 444 445 if models.GetExchangeType(exchange) == "DEX" { 446 numPairs = dexPoolCountMap[exchange.Name] 447 } else { 448 numPairs, err = env.RelDB.GetNumPairs(exchange) 449 if err != nil { 450 restApi.SendError(c, http.StatusInternalServerError, nil) 451 } 452 } 453 454 exchangereturn := exchangeReturn{ 455 Name: exchange.Name, 456 Volume24h: *vol, 457 Trades: numTrades, 458 Pairs: numPairs, 459 Blockchain: exchange.BlockChain.Name, 460 ScraperActive: exchange.ScraperActive, 461 } 462 exchangereturn.Type = models.GetExchangeType(exchange) 463 exchangereturns = append(exchangereturns, exchangereturn) 464 465 } 466 467 sort.Slice(exchangereturns, func(i, j int) bool { 468 return exchangereturns[i].Volume24h > exchangereturns[j].Volume24h 469 }) 470 471 c.JSON(http.StatusOK, exchangereturns) 472 } 473 474 // GetAssetChartPoints queries for filter points of asset given by address and blockchain. 475 func (env *Env) GetAssetChartPoints(c *gin.Context) { 476 if !validateInputParams(c) { 477 return 478 } 479 480 filter := c.Param("filter") 481 blockchain := c.Param("blockchain") 482 address := normalizeAddress(c.Param("address"), blockchain) 483 484 exchange := c.Query("exchange") 485 486 starttime, endtime, err := utils.MakeTimerange(c.Query("starttime"), c.Query("endtime"), time.Duration(7*24*time.Hour)) 487 if err != nil { 488 restApi.SendError(c, http.StatusInternalServerError, fmt.Errorf("parse time range")) 489 return 490 } 491 492 if ok := utils.ValidTimeRange(starttime, endtime, time.Duration(14*24*time.Hour)); !ok { 493 restApi.SendError(c, http.StatusInternalServerError, fmt.Errorf("time-range too big. max duration is %v", 14*24*time.Hour)) 494 return 495 } 496 497 p, err := env.DataStore.GetFilterPointsAsset(filter, exchange, address, blockchain, starttime, endtime) 498 if err != nil { 499 restApi.SendError(c, http.StatusInternalServerError, err) 500 } else { 501 c.JSON(http.StatusOK, p) 502 } 503 } 504 505 // GetChartPoints returns Filter points for given symbol -> Deprecated? 506 func (env *Env) GetChartPoints(c *gin.Context) { 507 if !validateInputParams(c) { 508 return 509 } 510 511 filter := c.Param("filter") 512 exchange := c.Param("exchange") 513 symbol := c.Param("symbol") 514 scale := c.Query("scale") 515 516 starttime, endtime, err := utils.MakeTimerange(c.Query("starttime"), c.Query("endtime"), time.Duration(7*24*time.Hour)) 517 if err != nil { 518 restApi.SendError(c, http.StatusInternalServerError, fmt.Errorf("parse time range")) 519 return 520 } 521 522 if ok := utils.ValidTimeRange(starttime, endtime, time.Duration(30*24*time.Hour)); !ok { 523 restApi.SendError(c, http.StatusInternalServerError, fmt.Errorf("time-range too big. max duration is %v", 30*24*time.Hour)) 524 return 525 } 526 527 p, err := env.DataStore.GetFilterPoints(filter, exchange, symbol, scale, starttime, endtime) 528 if err != nil { 529 restApi.SendError(c, http.StatusInternalServerError, err) 530 } else { 531 c.JSON(http.StatusOK, p) 532 } 533 } 534 535 // GetChartPointsAllExchanges returns filter points across all exchanges. 536 func (env *Env) GetChartPointsAllExchanges(c *gin.Context) { 537 if !validateInputParams(c) { 538 return 539 } 540 541 filter := c.Param("filter") 542 symbol := c.Param("symbol") 543 scale := c.Query("scale") 544 545 starttime, endtime, err := utils.MakeTimerange(c.Query("starttime"), c.Query("endtime"), time.Duration(7*24*time.Hour)) 546 if err != nil { 547 restApi.SendError(c, http.StatusInternalServerError, fmt.Errorf("parse time range")) 548 return 549 } 550 551 if ok := utils.ValidTimeRange(starttime, endtime, time.Duration(30*24*time.Hour)); !ok { 552 restApi.SendError(c, http.StatusInternalServerError, fmt.Errorf("time-range too big. max duration is %v", 30*24*time.Hour)) 553 return 554 } 555 556 p, err := env.DataStore.GetFilterPoints(filter, "", symbol, scale, starttime, endtime) 557 if err != nil { 558 restApi.SendError(c, http.StatusInternalServerError, err) 559 } else { 560 c.JSON(http.StatusOK, p) 561 } 562 563 } 564 565 func (env *Env) GetFilterPerSource(c *gin.Context) { 566 if !validateInputParams(c) { 567 return 568 } 569 570 type priceOnExchange struct { 571 Price float64 `json:"Price"` 572 Exchange string `json:"Exchange"` 573 Timestamp time.Time `json:"Time"` 574 } 575 576 type localReturn struct { 577 Asset dia.Asset `json:"Asset"` 578 Prices []priceOnExchange `json:"PricePerExchange"` 579 } 580 581 blockchain := c.Param("blockchain") 582 address := normalizeAddress(c.Param("address"), blockchain) 583 filter := c.Param("filter") 584 585 starttime, endtime, err := utils.MakeTimerange(c.Query("starttime"), c.Query("endtime"), time.Duration(30)*time.Minute) 586 if err != nil { 587 restApi.SendError(c, http.StatusInternalServerError, nil) 588 return 589 } 590 591 assetQuotations, err := env.DataStore.GetFilterAllExchanges(filter, address, blockchain, starttime, endtime) 592 if err != nil { 593 restApi.SendError(c, http.StatusInternalServerError, nil) 594 return 595 } 596 597 var lr localReturn 598 lr.Asset = env.getAssetFromCache(ASSET_CACHE, blockchain, address) 599 600 for _, aq := range assetQuotations { 601 var pe priceOnExchange 602 pe.Exchange = aq.Source 603 pe.Price = aq.Price 604 pe.Timestamp = aq.Time 605 lr.Prices = append(lr.Prices, pe) 606 } 607 c.JSON(http.StatusOK, lr) 608 609 } 610 611 // GetAllSymbols returns all Symbols on @exchange. 612 // If @exchange is not set, it returns all symbols across all exchanges. 613 // If @top is set to an integer, only the top @top symbols w.r.t. trading volume are returned. This is 614 // only active if @exchange is not set. 615 func (env *Env) GetAllSymbols(c *gin.Context) { 616 if !validateInputParams(c) { 617 return 618 } 619 620 var ( 621 s []string 622 numSymbols int64 623 sortedAssets []dia.AssetVolume 624 err error 625 ) 626 627 substring := c.Param("substring") 628 exchange := c.DefaultQuery("exchange", "noRange") 629 numSymbolsString := c.Query("top") 630 631 if numSymbolsString != "" { 632 numSymbols, err = strconv.ParseInt(numSymbolsString, 10, 64) 633 if err != nil { 634 restApi.SendError(c, http.StatusInternalServerError, errors.New("number of symbols must be an integer")) 635 } 636 } 637 638 // Filter results by substring. @exchange is disabled. 639 if substring != "" { 640 s, err = env.RelDB.GetExchangeSymbols("", substring) 641 if err != nil { 642 restApi.SendError(c, http.StatusInternalServerError, errors.New("cannot find symbols")) 643 } 644 s = utils.UniqueStrings(s) 645 646 sort.Strings(s) 647 // Sort all symbols by volume, append if they have no volume. 648 sortedAssets, err = env.RelDB.GetSortedAssetSymbols(int64(0), int64(0), substring) 649 if err != nil { 650 log.Error("get assets with volume: ", err) 651 } 652 var sortedSymbols []string 653 for _, assetvol := range sortedAssets { 654 sortedSymbols = append(sortedSymbols, assetvol.Asset.Symbol) 655 } 656 sortedSymbols = utils.UniqueStrings(sortedSymbols) 657 allSymbols := utils.UniqueStrings(append(sortedSymbols, s...)) 658 659 c.JSON(http.StatusOK, allSymbols) 660 return 661 } 662 663 if exchange == "noRange" { 664 if numSymbolsString != "" { 665 // -- Get top @numSymbols symbols across all exchanges. -- 666 sortedAssets, err = env.RelDB.GetAssetsWithVOL(time.Now().AddDate(0, -1, 0), numSymbols, int64(0), false, "") 667 if err != nil { 668 log.Error("get assets with volume: ", err) 669 } 670 for _, assetvol := range sortedAssets { 671 s = append(s, assetvol.Asset.Symbol) 672 } 673 c.JSON(http.StatusOK, s) 674 } else { 675 // -- Get all symbols across all exchanges. -- 676 s, err = env.RelDB.GetExchangeSymbols("", "") 677 if err != nil { 678 restApi.SendError(c, http.StatusInternalServerError, errors.New("cannot find symbols")) 679 } 680 s = utils.UniqueStrings(s) 681 682 sort.Strings(s) 683 // Sort all symbols by volume, append if they have no volume. 684 sortedAssets, err = env.RelDB.GetAssetsWithVOL(time.Now().AddDate(0, -1, 0), numSymbols, int64(0), false, "") 685 if err != nil { 686 log.Error("get assets with volume: ", err) 687 } 688 var sortedSymbols []string 689 for _, assetvol := range sortedAssets { 690 sortedSymbols = append(sortedSymbols, assetvol.Asset.Symbol) 691 } 692 sortedSymbols = utils.UniqueStrings(sortedSymbols) 693 allSymbols := utils.UniqueStrings(append(sortedSymbols, s...)) 694 695 c.JSON(http.StatusOK, allSymbols) 696 } 697 } else { 698 // -- Get all symbols on @exchange. -- 699 symbols, err := env.RelDB.GetExchangeSymbols(exchange, "") 700 if err != nil { 701 restApi.SendError(c, http.StatusInternalServerError, errors.New("cannot find symbols")) 702 } 703 c.JSON(http.StatusOK, symbols) 704 } 705 706 } 707 708 // ----------------------------------------------------------------------------- 709 // POOLS AND LIQUIDITY 710 // ----------------------------------------------------------------------------- 711 712 func (env *Env) GetPoolsByAsset(c *gin.Context) { 713 if !validateInputParams(c) { 714 return 715 } 716 blockchain := c.Param("blockchain") 717 address := normalizeAddress(c.Param("address"), blockchain) 718 asset := env.getAssetFromCache(ASSET_CACHE, blockchain, address) 719 720 liquidityThreshold, err := strconv.ParseFloat(c.DefaultQuery("liquidityThreshold", "10"), 64) 721 if err != nil { 722 restApi.SendError(c, http.StatusInternalServerError, errors.New("cannot parse liquidityThreshold")) 723 return 724 } 725 726 liquidityThresholdUSD, err := strconv.ParseFloat(c.DefaultQuery("liquidityThresholdUSD", "10000"), 64) 727 if err != nil { 728 restApi.SendError(c, http.StatusInternalServerError, errors.New("cannot parse liquidityThresholdUSD")) 729 return 730 } 731 732 // Set liquidity threshold measured in native currency to 1 in order to filter out noise. 733 pools, err := env.RelDB.GetPoolsByAsset(asset, liquidityThreshold, 0) 734 if err != nil { 735 restApi.SendError(c, http.StatusInternalServerError, errors.New("cannot find pool")) 736 return 737 } 738 739 type poolInfo struct { 740 Exchange string 741 Blockchain string 742 Address string 743 Time time.Time 744 TotalLiquidityUSD float64 745 Message string 746 Liquidity []dia.AssetLiquidity 747 } 748 var result []poolInfo 749 750 // Get total liquidity for each filtered pool. 751 for _, pool := range pools { 752 var pi poolInfo 753 754 totalLiquidity, lowerBound := pool.GetPoolLiquidityUSD() 755 756 // In case we can determine USD liquidity and it's below the threshold, continue. 757 if !lowerBound && totalLiquidity < liquidityThresholdUSD { 758 continue 759 } 760 761 pi.Exchange = pool.Exchange.Name 762 pi.Blockchain = pool.Blockchain.Name 763 pi.Address = pool.Address 764 pi.TotalLiquidityUSD = totalLiquidity 765 pi.Time = pool.Time 766 for i := range pool.Assetvolumes { 767 var al dia.AssetLiquidity = dia.AssetLiquidity(pool.Assetvolumes[i]) 768 pi.Liquidity = append(pi.Liquidity, al) 769 } 770 if lowerBound { 771 pi.Message = "No US-Dollar price information on one or more pool assets available." 772 } 773 result = append(result, pi) 774 } 775 776 // Sort by total USD liquidity. 777 sort.Slice(result, func(m, n int) bool { 778 return result[m].TotalLiquidityUSD > result[n].TotalLiquidityUSD 779 }) 780 781 c.JSON(http.StatusOK, result) 782 } 783 784 func (env *Env) GetPoolLiquidityByAddress(c *gin.Context) { 785 if !validateInputParams(c) { 786 return 787 } 788 blockchain := c.Param("blockchain") 789 address := normalizeAddress(c.Param("address"), blockchain) 790 791 pool, err := env.RelDB.GetPoolByAddress(blockchain, address) 792 if err != nil { 793 log.Info("err: ", err) 794 restApi.SendError(c, http.StatusInternalServerError, errors.New("cannot find pool")) 795 return 796 } 797 798 // Get total liquidity. 799 var ( 800 totalLiquidity float64 801 lowerBound bool 802 ) 803 totalLiquidity, lowerBound = pool.GetPoolLiquidityUSD() 804 805 type localReturn struct { 806 Exchange string 807 Blockchain string 808 Address string 809 Time time.Time 810 TotalLiquidityUSD float64 811 Message string 812 Liquidity []dia.AssetLiquidity 813 } 814 815 var l localReturn 816 if lowerBound { 817 l.Message = "No US-Dollar price information on one or more pool assets available." 818 } 819 l.TotalLiquidityUSD = totalLiquidity 820 l.Exchange = pool.Exchange.Name 821 l.Blockchain = pool.Blockchain.Name 822 l.Address = pool.Address 823 l.Time = pool.Time 824 for i := range pool.Assetvolumes { 825 var al dia.AssetLiquidity = dia.AssetLiquidity(pool.Assetvolumes[i]) 826 l.Liquidity = append(l.Liquidity, al) 827 } 828 829 c.JSON(http.StatusOK, l) 830 831 } 832 833 func (env *Env) GetPoolSlippage(c *gin.Context) { 834 if !validateInputParams(c) { 835 return 836 } 837 blockchain := c.Param("blockchain") 838 addressPool := normalizeAddress(c.Param("addressPool"), blockchain) 839 addressAsset := normalizeAddress(c.Param("addressAsset"), blockchain) 840 poolType := c.Param("poolType") 841 priceDeviationInt, err := strconv.ParseInt(c.Param("priceDeviation"), 10, 64) 842 if err != nil { 843 restApi.SendError(c, http.StatusInternalServerError, errors.New("error parsing priceDeviation")) 844 return 845 } 846 if priceDeviationInt < 0 || priceDeviationInt >= 1000 { 847 restApi.SendError(c, http.StatusInternalServerError, errors.New("priceDeviation measured in per mille is out of range")) 848 return 849 } 850 priceDeviation := float64(priceDeviationInt) / 1000 851 852 type localReturn struct { 853 VolumeRequired float64 854 AssetIn string 855 Exchange string 856 Blockchain string 857 Address string 858 Time time.Time 859 Liquidity []dia.AssetLiquidity 860 } 861 862 pool, err := env.RelDB.GetPoolByAddress(blockchain, addressPool) 863 if err != nil { 864 restApi.SendError(c, http.StatusInternalServerError, errors.New("cannot find pool")) 865 return 866 } 867 var l localReturn 868 l.Exchange = pool.Exchange.Name 869 l.Blockchain = pool.Blockchain.Name 870 l.Address = pool.Address 871 l.Time = pool.Time 872 for i := range pool.Assetvolumes { 873 var al dia.AssetLiquidity = dia.AssetLiquidity(pool.Assetvolumes[i]) 874 l.Liquidity = append(l.Liquidity, al) 875 } 876 877 var ( 878 assetInIndex int 879 foundAsset bool 880 ) 881 for i := range pool.Assetvolumes { 882 if pool.Assetvolumes[i].Asset.Address == addressAsset { 883 assetInIndex = i 884 foundAsset = true 885 } 886 } 887 if !foundAsset { 888 restApi.SendError(c, http.StatusInternalServerError, fmt.Errorf("asset %s not in pool", addressAsset)) 889 return 890 } 891 l.AssetIn = pool.Assetvolumes[assetInIndex].Asset.Symbol 892 893 switch poolType { 894 case "UniswapV2": 895 l.VolumeRequired = pool.Assetvolumes[assetInIndex].Volume * (1/(1-priceDeviation) - 1) 896 } 897 898 c.JSON(http.StatusOK, l) 899 } 900 901 func (env *Env) GetPoolPriceImpact(c *gin.Context) { 902 if !validateInputParams(c) { 903 return 904 } 905 blockchain := c.Param("blockchain") 906 addressPool := normalizeAddress(c.Param("addressPool"), blockchain) 907 addressAsset := normalizeAddress(c.Param("addressAsset"), blockchain) 908 poolType := c.Param("poolType") 909 priceDeviationInt, err := strconv.ParseInt(c.Param("priceDeviation"), 10, 64) 910 if err != nil { 911 restApi.SendError(c, http.StatusInternalServerError, errors.New("error parsing priceDeviation")) 912 return 913 } 914 if priceDeviationInt < 0 || priceDeviationInt >= 1000 { 915 restApi.SendError(c, http.StatusInternalServerError, errors.New("priceDeviation measured in per mille is out of range")) 916 return 917 } 918 priceDeviation := float64(priceDeviationInt) / 1000 919 920 type localReturn struct { 921 VolumeRequired float64 922 AssetIn string 923 Exchange string 924 Blockchain string 925 Address string 926 Time time.Time 927 Liquidity []dia.AssetLiquidity 928 } 929 930 pool, err := env.RelDB.GetPoolByAddress(blockchain, addressPool) 931 if err != nil { 932 restApi.SendError(c, http.StatusInternalServerError, errors.New("cannot find pool")) 933 return 934 } 935 var l localReturn 936 l.Exchange = pool.Exchange.Name 937 l.Blockchain = pool.Blockchain.Name 938 l.Address = pool.Address 939 l.Time = pool.Time 940 for i := range pool.Assetvolumes { 941 var al dia.AssetLiquidity = dia.AssetLiquidity(pool.Assetvolumes[i]) 942 l.Liquidity = append(l.Liquidity, al) 943 } 944 945 var ( 946 assetInIndex int 947 foundAsset bool 948 ) 949 for i := range pool.Assetvolumes { 950 if pool.Assetvolumes[i].Asset.Address == addressAsset { 951 assetInIndex = i 952 foundAsset = true 953 } 954 } 955 if !foundAsset { 956 restApi.SendError(c, http.StatusInternalServerError, fmt.Errorf("asset %s not in pool", addressAsset)) 957 return 958 } 959 l.AssetIn = pool.Assetvolumes[assetInIndex].Asset.Symbol 960 961 switch poolType { 962 case "UniswapV2": 963 l.VolumeRequired = pool.Assetvolumes[assetInIndex].Volume * (1/math.Sqrt(1-priceDeviation) - 1) 964 } 965 966 c.JSON(http.StatusOK, l) 967 } 968 969 func (env *Env) GetPriceImpactSimulation(c *gin.Context) { 970 if !validateInputParams(c) { 971 return 972 } 973 974 poolType := c.Param("poolType") 975 priceDeviationInt, err := strconv.ParseInt(c.Param("priceDeviation"), 10, 64) 976 if err != nil { 977 restApi.SendError(c, http.StatusInternalServerError, errors.New("error parsing priceDeviation")) 978 return 979 } 980 if priceDeviationInt < 0 || priceDeviationInt >= 1000 { 981 restApi.SendError(c, http.StatusInternalServerError, errors.New("priceDeviation measured in per mille is out of range")) 982 return 983 } 984 priceDeviation := float64(priceDeviationInt) / 1000 985 liquidityA, err := strconv.ParseFloat(c.Param("liquidityA"), 64) 986 if err != nil { 987 restApi.SendError(c, http.StatusInternalServerError, errors.New("error parsing liquidityA")) 988 return 989 } 990 if liquidityA <= 0 { 991 restApi.SendError(c, http.StatusInternalServerError, errors.New("liquidity must be a non-negative number")) 992 return 993 } 994 liquidityB, err := strconv.ParseFloat(c.Param("liquidityB"), 64) 995 if err != nil { 996 restApi.SendError(c, http.StatusInternalServerError, errors.New("error parsing liquidityB")) 997 return 998 } 999 if liquidityB <= 0 { 1000 restApi.SendError(c, http.StatusInternalServerError, errors.New("liquidity must be a non-negative number")) 1001 return 1002 } 1003 1004 type dummyLiquidity struct { 1005 Asset string 1006 Liquidity float64 1007 } 1008 1009 type localReturn struct { 1010 PriceDeviation float64 1011 PriceAssetA float64 1012 PriceAssetB float64 1013 VolumesRequired []struct { 1014 AssetIn string 1015 VolumeRequired float64 1016 InitialPriceAssetIn float64 1017 ResultingPriceAssetIn float64 1018 ResultingPriceAssetOut float64 1019 } 1020 Liquidity []dummyLiquidity 1021 } 1022 1023 l := []dummyLiquidity{ 1024 {Asset: "A", Liquidity: liquidityA}, 1025 {Asset: "B", Liquidity: liquidityB}, 1026 } 1027 var lr localReturn 1028 lr.PriceDeviation = priceDeviation 1029 lr.PriceAssetA = liquidityB / liquidityA 1030 lr.PriceAssetB = liquidityA / liquidityB 1031 1032 switch poolType { 1033 case "UniswapV2": 1034 volRequiredA := liquidityA * (1/math.Sqrt(1-priceDeviation) - 1) 1035 volRequiredB := liquidityB * (1/math.Sqrt(1-priceDeviation) - 1) 1036 lr.VolumesRequired = append(lr.VolumesRequired, struct { 1037 AssetIn string 1038 VolumeRequired float64 1039 InitialPriceAssetIn float64 1040 ResultingPriceAssetIn float64 1041 ResultingPriceAssetOut float64 1042 }{ 1043 "A", 1044 volRequiredA, 1045 liquidityB / liquidityA, 1046 liquidityA * liquidityB / math.Pow(volRequiredA+liquidityA, 2), 1047 math.Pow(volRequiredA+liquidityA, 2) / liquidityA / liquidityB, 1048 }) 1049 lr.Liquidity = l 1050 lr.VolumesRequired = append(lr.VolumesRequired, struct { 1051 AssetIn string 1052 VolumeRequired float64 1053 InitialPriceAssetIn float64 1054 ResultingPriceAssetIn float64 1055 ResultingPriceAssetOut float64 1056 }{ 1057 "B", 1058 volRequiredB, 1059 liquidityA / liquidityB, 1060 liquidityB * liquidityA / math.Pow(volRequiredB+liquidityB, 2), 1061 math.Pow(volRequiredB+liquidityB, 2) / liquidityA / liquidityB, 1062 }) 1063 1064 } 1065 1066 c.JSON(http.StatusOK, lr) 1067 } 1068 1069 // ----------------------------------------------------------------------------- 1070 // EXCHANGE PAIRS 1071 // ----------------------------------------------------------------------------- 1072 1073 func (env *Env) GetExchangePairs(c *gin.Context) { 1074 if !validateInputParams(c) { 1075 return 1076 } 1077 exchange, err := env.RelDB.GetExchange(c.Param("exchange")) 1078 if err != nil { 1079 restApi.SendError(c, http.StatusInternalServerError, err) 1080 return 1081 } 1082 var ( 1083 filterVerified bool 1084 verified bool 1085 ) 1086 verifiedString := c.Query("verified") 1087 if verifiedString != "" { 1088 verified, err = strconv.ParseBool(verifiedString) 1089 if err != nil { 1090 restApi.SendError(c, http.StatusInternalServerError, err) 1091 return 1092 } 1093 filterVerified = true 1094 } 1095 1096 pairs, err := env.RelDB.GetPairsForExchange(exchange, filterVerified, verified) 1097 if err != nil { 1098 restApi.SendError(c, http.StatusInternalServerError, err) 1099 return 1100 } 1101 1102 sort.Slice(pairs, func(m, n int) bool { 1103 return pairs[m].Symbol < pairs[n].Symbol 1104 }) 1105 c.JSON(http.StatusOK, pairs) 1106 1107 } 1108 1109 func (env *Env) GetAssetPairs(c *gin.Context) { 1110 if !validateInputParams(c) { 1111 return 1112 } 1113 blockchain := c.Param("blockchain") 1114 address := normalizeAddress(c.Param("address"), blockchain) 1115 var ( 1116 filterVerified bool 1117 verified bool 1118 err error 1119 ) 1120 verifiedString := c.Query("verified") 1121 if verifiedString != "" { 1122 verified, err = strconv.ParseBool(verifiedString) 1123 if err != nil { 1124 restApi.SendError(c, http.StatusInternalServerError, err) 1125 return 1126 } 1127 filterVerified = true 1128 } 1129 1130 pairs, err := env.RelDB.GetPairsForAsset(dia.Asset{Address: address, Blockchain: blockchain}, filterVerified, verified) 1131 if err != nil { 1132 restApi.SendError(c, http.StatusInternalServerError, err) 1133 return 1134 } 1135 1136 sort.Slice(pairs, func(m, n int) bool { return pairs[m].Exchange < pairs[n].Exchange }) 1137 c.JSON(http.StatusOK, pairs) 1138 1139 } 1140 1141 func (env *Env) SearchAsset(c *gin.Context) { 1142 if !validateInputParams(c) { 1143 return 1144 } 1145 1146 querystring := c.Param("query") 1147 var ( 1148 assets = []dia.Asset{} 1149 err error 1150 ) 1151 1152 switch { 1153 case len(querystring) > 4 && strings.Contains(querystring[0:2], "0x"): 1154 assets, err = env.RelDB.GetAssetsByAddress(querystring) 1155 if err != nil { 1156 // restApi.SendError(c, http.StatusInternalServerError, errors.New("eror getting asset")) 1157 log.Errorln("error getting GetAssetsByAddress", err) 1158 } 1159 1160 case len(querystring) > 4 && !strings.Contains(querystring[0:2], "0x"): 1161 assets, err = env.RelDB.GetAssetsBySymbolName(querystring, querystring) 1162 if err != nil { 1163 // restApi.SendError(c, http.StatusInternalServerError, errors.New("eror getting asset")) 1164 log.Errorln("error getting GetAssetsBySymbolName", err) 1165 1166 } 1167 1168 case len(querystring) <= 4: 1169 assets, err = env.RelDB.GetAssetsBySymbolName(querystring, querystring) 1170 if err != nil { 1171 // restApi.SendError(c, http.StatusInternalServerError, errors.New("eror getting asset")) 1172 log.Errorln("error getting GetAssetsBySymbolName", err) 1173 1174 } 1175 } 1176 c.JSON(http.StatusOK, assets) 1177 } 1178 1179 func (env *Env) GetTopAssets(c *gin.Context) { 1180 if !validateInputParams(c) { 1181 return 1182 } 1183 1184 numAssetsString := c.Param("numAssets") 1185 pageString := c.DefaultQuery("Page", "1") 1186 onlycexString := c.DefaultQuery("Cex", "false") 1187 blockchain := c.DefaultQuery("Network", "") 1188 1189 var ( 1190 numAssets int64 1191 sortedAssets []dia.AssetVolume 1192 err error 1193 pageNumber int64 1194 offset int64 1195 ) 1196 1197 pageNumber, err = strconv.ParseInt(pageString, 10, 64) 1198 if err != nil { 1199 restApi.SendError(c, http.StatusInternalServerError, errors.New("page of assets must be an integer")) 1200 } 1201 1202 onlycex, err := strconv.ParseBool(onlycexString) 1203 if err != nil { 1204 log.Fatal(err) 1205 } 1206 1207 numAssets, err = strconv.ParseInt(numAssetsString, 10, 64) 1208 if err != nil { 1209 restApi.SendError(c, http.StatusInternalServerError, errors.New("number of assets must be an integer")) 1210 } 1211 1212 offset = (pageNumber - 1) * numAssets 1213 1214 sortedAssets, err = env.RelDB.GetAssetsWithVOL(time.Now().AddDate(0, 0, -7), numAssets, offset, onlycex, blockchain) 1215 if err != nil { 1216 log.Error("get assets with volume: ", err) 1217 1218 } 1219 var assets = []dia.TopAsset{} 1220 1221 for _, v := range sortedAssets { 1222 var sources = make(map[string][]string) 1223 1224 aqf := dia.TopAsset{} 1225 aqf.Asset = v.Asset 1226 quotation, err := env.DataStore.GetAssetQuotationLatest(aqf.Asset, dia.CRYPTO_ZERO_UNIX_TIME) 1227 if err != nil { 1228 log.Warn("quotation: ", err) 1229 } else { 1230 aqf.Price = quotation.Price 1231 1232 } 1233 aqf.Volume = v.Volume 1234 1235 sources["CEX"], err = env.RelDB.GetAssetSource(v.Asset, true) 1236 if err != nil { 1237 log.Warn("get GetAssetSource: ", err) 1238 } 1239 sources["DEX"], err = env.RelDB.GetAssetSource(v.Asset, false) 1240 if err != nil { 1241 log.Warn("get GetAssetSource: ", err) 1242 } 1243 aqf.Source = sources 1244 1245 quotationYesterday, err := env.DataStore.GetAssetQuotation(aqf.Asset, dia.CRYPTO_ZERO_UNIX_TIME, time.Now().AddDate(0, 0, -1)) 1246 if err != nil { 1247 log.Warn("get quotation yesterday: ", err) 1248 } else { 1249 aqf.PriceYesterday = quotationYesterday.Price 1250 } 1251 1252 assets = append(assets, aqf) 1253 1254 } 1255 c.JSON(http.StatusOK, assets) 1256 } 1257 1258 // GetQuotedAssets is the delegate method to fetch all assets that have an asset quotation 1259 // dating back at most 7 days. 1260 func (env *Env) GetQuotedAssets(c *gin.Context) { 1261 if !validateInputParams(c) { 1262 return 1263 } 1264 1265 endtime := time.Now() 1266 starttime := endtime.AddDate(0, 0, -7) 1267 assetvolumes, err := env.RelDB.GetAssetsWithVolByBlockchain(starttime, endtime, c.Query("blockchain")) 1268 if err != nil { 1269 log.Error("get assets with volume: ", err) 1270 } 1271 1272 c.JSON(http.StatusOK, assetvolumes) 1273 } 1274 1275 // ----------------------------------------------------------------------------- 1276 // FIAT CURRENCIES 1277 // ----------------------------------------------------------------------------- 1278 1279 // GetFiatQuotations returns several quotations vs USD as published by the ECB 1280 func (env *Env) GetFiatQuotations(c *gin.Context) { 1281 if !validateInputParams(c) { 1282 return 1283 } 1284 q, err := env.DataStore.GetCurrencyChange() 1285 if err != nil { 1286 if errors.Is(err, redis.Nil) { 1287 restApi.SendError(c, http.StatusNotFound, err) 1288 } else { 1289 restApi.SendError(c, http.StatusInternalServerError, err) 1290 } 1291 } else { 1292 c.JSON(http.StatusOK, q) 1293 } 1294 } 1295 1296 func (env *Env) GetTwelvedataFiatQuotations(c *gin.Context) { 1297 if !validateInputParams(c) { 1298 return 1299 } 1300 1301 // Parse symbol. 1302 assets := strings.Split(c.Param("symbol"), "-") 1303 if len(assets) != 2 { 1304 restApi.SendError(c, http.StatusNotFound, errors.New("wrong format for forex pair")) 1305 return 1306 } 1307 symbol := assets[0] + "/" + assets[1] 1308 1309 // Time for quotation is time.Now() by default. 1310 timestampInt, err := strconv.ParseInt(c.DefaultQuery("timestamp", strconv.Itoa(int(time.Now().Unix()))), 10, 64) 1311 if err != nil { 1312 restApi.SendError(c, http.StatusNotFound, errors.New("could not parse Unix timestamp")) 1313 return 1314 } 1315 timestamp := time.Unix(timestampInt, 0) 1316 1317 var ( 1318 q models.ForeignQuotation 1319 errRev error 1320 reverse bool 1321 ) 1322 1323 q, err = env.DataStore.GetForeignQuotationInflux(symbol, "TwelveData", timestamp) 1324 if err != nil || q.Price == 0 { 1325 reverse = true 1326 symbol = assets[1] + "/" + assets[0] 1327 log.Info("try reverse order: ", symbol) 1328 q, errRev = env.DataStore.GetForeignQuotationInflux(symbol, "TwelveData", timestamp) 1329 if errRev != nil || q.Price == 0 { 1330 if q.Price == 0 { 1331 errRev = errors.New("not found") 1332 } 1333 if errors.Is(errRev, redis.Nil) { 1334 restApi.SendError(c, http.StatusNotFound, errRev) 1335 return 1336 } else { 1337 log.Info(c) 1338 restApi.SendError(c, http.StatusInternalServerError, errRev) 1339 return 1340 } 1341 } 1342 } 1343 1344 response := struct { 1345 Ticker string 1346 Price float64 1347 Timestamp time.Time 1348 }{ 1349 Ticker: c.Param("symbol"), 1350 Price: q.Price, 1351 Timestamp: q.Time, 1352 } 1353 if err == nil && !reverse { 1354 c.JSON(http.StatusOK, response) 1355 return 1356 } 1357 if errRev == nil && q.Price != 0 { 1358 response.Price = 1 / q.Price 1359 c.JSON(http.StatusOK, response) 1360 } 1361 } 1362 1363 // ----------------------------------------------------------------------------- 1364 // STOCKS 1365 // ----------------------------------------------------------------------------- 1366 1367 func (env *Env) GetTwelvedataStockQuotations(c *gin.Context) { 1368 if !validateInputParams(c) { 1369 return 1370 } 1371 1372 // Time for quotation is time.Now() by default. 1373 timestampInt, err := strconv.ParseInt(c.DefaultQuery("timestamp", strconv.Itoa(int(time.Now().Unix()))), 10, 64) 1374 if err != nil { 1375 restApi.SendError(c, http.StatusNotFound, errors.New("could not parse Unix timestamp")) 1376 return 1377 } 1378 timestamp := time.Unix(timestampInt, 0) 1379 1380 q, err := env.DataStore.GetForeignQuotationInflux(c.Param("symbol"), "TwelveData", timestamp) 1381 if err != nil { 1382 if errors.Is(err, redis.Nil) { 1383 restApi.SendError(c, http.StatusNotFound, err) 1384 } else { 1385 restApi.SendError(c, http.StatusInternalServerError, err) 1386 } 1387 } else { 1388 // Format response. 1389 response := struct { 1390 Ticker string 1391 Price float64 1392 Timestamp time.Time 1393 }{ 1394 Ticker: c.Param("symbol"), 1395 Price: q.Price, 1396 Timestamp: q.Time, 1397 } 1398 c.JSON(http.StatusOK, response) 1399 } 1400 } 1401 1402 func (env *Env) GetStockSymbols(c *gin.Context) { 1403 if !validateInputParams(c) { 1404 return 1405 } 1406 type sourcedStock struct { 1407 Stock models.Stock 1408 Source string 1409 } 1410 var srcStocks []sourcedStock 1411 stocks, err := env.DataStore.GetStockSymbols() 1412 log.Info("stocks: ", stocks) 1413 1414 if err != nil { 1415 if errors.Is(err, redis.Nil) { 1416 restApi.SendError(c, http.StatusNotFound, err) 1417 } else { 1418 restApi.SendError(c, http.StatusInternalServerError, err) 1419 } 1420 } else { 1421 for stock, source := range stocks { 1422 srcStocks = append(srcStocks, sourcedStock{ 1423 Stock: stock, 1424 Source: source, 1425 }) 1426 } 1427 c.JSON(http.StatusOK, srcStocks) 1428 } 1429 } 1430 1431 // GetStockQuotation is the delegate method to fetch the value(s) of 1432 // quotations of asset with @symbol from @source. 1433 // Last value is retrieved. Otional query parameters allow to obtain data in a time range. 1434 func (env *Env) GetStockQuotation(c *gin.Context) { 1435 if !validateInputParams(c) { 1436 return 1437 } 1438 source := c.Param("source") 1439 symbol := c.Param("symbol") 1440 date := c.Param("time") 1441 // Add optional query parameters for requesting a range of values 1442 dateInit := c.DefaultQuery("dateInit", "noRange") 1443 dateFinal := c.Query("dateFinal") 1444 1445 if dateInit == "noRange" { 1446 // Return most recent data point 1447 var endTime time.Time 1448 var err error 1449 if date == "" { 1450 endTime = time.Now() 1451 } else { 1452 // Convert unix time int/string to time 1453 endTime, err = utils.StrToUnixtime(date) 1454 if err != nil { 1455 restApi.SendError(c, http.StatusNotFound, err) 1456 } 1457 } 1458 startTime := endTime.AddDate(0, 0, -1) 1459 1460 q, err := env.DataStore.GetStockQuotation(source, symbol, startTime, endTime) 1461 if err != nil { 1462 if errors.Is(err, redis.Nil) { 1463 restApi.SendError(c, http.StatusNotFound, err) 1464 } else { 1465 restApi.SendError(c, http.StatusInternalServerError, err) 1466 } 1467 } else { 1468 c.JSON(http.StatusOK, q[0]) 1469 } 1470 } else { 1471 starttime, err := utils.StrToUnixtime(dateInit) 1472 if err != nil { 1473 restApi.SendError(c, http.StatusNotFound, err) 1474 } 1475 endtime, err := utils.StrToUnixtime(dateFinal) 1476 if err != nil { 1477 restApi.SendError(c, http.StatusNotFound, err) 1478 } 1479 1480 q, err := env.DataStore.GetStockQuotation(source, symbol, starttime, endtime) 1481 if err != nil { 1482 if errors.Is(err, redis.Nil) { 1483 restApi.SendError(c, http.StatusNotFound, err) 1484 } else { 1485 restApi.SendError(c, http.StatusInternalServerError, err) 1486 } 1487 } else { 1488 c.JSON(http.StatusOK, q) 1489 } 1490 } 1491 } 1492 1493 func (env *Env) GetTwelvedataCommodityQuotation(c *gin.Context) { 1494 if !validateInputParams(c) { 1495 return 1496 } 1497 1498 timestampInt, err := strconv.ParseInt(c.DefaultQuery("timestamp", strconv.Itoa(int(time.Now().Unix()))), 10, 64) 1499 if err != nil { 1500 restApi.SendError(c, http.StatusNotFound, errors.New("could not parse Unix timestamp")) 1501 return 1502 } 1503 timestamp := time.Unix(timestampInt, 0) 1504 if len(strings.Split(c.Param("symbol"), "-")) != 2 { 1505 restApi.SendError(c, http.StatusNotFound, errors.New("symbol format not known")) 1506 return 1507 } 1508 symbol := strings.Split(c.Param("symbol"), "-")[0] + "/" + strings.Split(c.Param("symbol"), "-")[1] 1509 1510 q, err := env.DataStore.GetForeignQuotationInflux(symbol, "TwelveData", timestamp) 1511 if err != nil { 1512 if errors.Is(err, redis.Nil) { 1513 restApi.SendError(c, http.StatusNotFound, err) 1514 } else { 1515 restApi.SendError(c, http.StatusInternalServerError, err) 1516 } 1517 } else { 1518 // Format response. 1519 response := struct { 1520 Ticker string 1521 Name string 1522 Price float64 1523 Timestamp time.Time 1524 }{ 1525 Ticker: c.Param("symbol"), 1526 Name: q.Name, 1527 Price: q.Price, 1528 Timestamp: q.Time, 1529 } 1530 c.JSON(http.StatusOK, response) 1531 } 1532 } 1533 1534 func (env *Env) GetTwelvedataETFQuotation(c *gin.Context) { 1535 if !validateInputParams(c) { 1536 return 1537 } 1538 1539 timestampInt, err := strconv.ParseInt(c.DefaultQuery("timestamp", strconv.Itoa(int(time.Now().Unix()))), 10, 64) 1540 if err != nil { 1541 restApi.SendError(c, http.StatusNotFound, errors.New("could not parse Unix timestamp")) 1542 return 1543 } 1544 timestamp := time.Unix(timestampInt, 0) 1545 1546 q, err := env.DataStore.GetForeignQuotationInflux(c.Param("symbol"), "TwelveData", timestamp) 1547 if err != nil { 1548 if errors.Is(err, redis.Nil) { 1549 restApi.SendError(c, http.StatusNotFound, err) 1550 } else { 1551 restApi.SendError(c, http.StatusInternalServerError, err) 1552 } 1553 } else { 1554 // Format response. 1555 response := struct { 1556 Ticker string 1557 Name string 1558 Price float64 1559 Timestamp time.Time 1560 }{ 1561 Ticker: c.Param("symbol"), 1562 Name: q.Name, 1563 Price: q.Price, 1564 Timestamp: q.Time, 1565 } 1566 c.JSON(http.StatusOK, response) 1567 } 1568 } 1569 1570 // ----------------------------------------------------------------------------- 1571 // FOREIGN QUOTATIONS 1572 // ----------------------------------------------------------------------------- 1573 1574 // GetForeignQuotation returns several quotations vs USD as published by the ECB 1575 func (env *Env) GetForeignQuotation(c *gin.Context) { 1576 if !validateInputParams(c) { 1577 return 1578 } 1579 var ( 1580 q models.ForeignQuotation 1581 ) 1582 1583 source := c.Param("source") 1584 symbol := c.Param("symbol") 1585 date := c.DefaultQuery("time", "noRange") 1586 var timestamp time.Time 1587 1588 if date == "noRange" { 1589 timestamp = time.Now() 1590 } else { 1591 t, err := strconv.Atoi(date) 1592 if err != nil { 1593 log.Error(err) 1594 } 1595 timestamp = time.Unix(int64(t), 0) 1596 } 1597 1598 var err error 1599 q, err = env.DataStore.GetForeignQuotationInflux(symbol, source, timestamp) 1600 if err != nil || q.Time.Before(time.Unix(1689847252, 0)) { 1601 // Attempt to fetch quotation for reversed order of symbol string. 1602 assetsSymbols := strings.Split(symbol, "-") 1603 if source == "YahooFinance" && len(assetsSymbols) == 2 { 1604 symbolInflux := assetsSymbols[1] + "-" + assetsSymbols[0] 1605 q, err = env.DataStore.GetForeignQuotationInflux(symbolInflux, source, timestamp) 1606 if err != nil { 1607 restApi.SendError(c, http.StatusInternalServerError, err) 1608 return 1609 } 1610 if q.Price != 0 { 1611 q.Price = 1 / q.Price 1612 } 1613 if q.PriceYesterday != 0 { 1614 q.PriceYesterday = 1 / q.PriceYesterday 1615 } 1616 q.Symbol = symbol 1617 q.Name = symbol 1618 c.JSON(http.StatusOK, q) 1619 } else { 1620 restApi.SendError(c, http.StatusInternalServerError, err) 1621 return 1622 } 1623 } else { 1624 c.JSON(http.StatusOK, q) 1625 } 1626 } 1627 1628 // GetForeignSymbols returns all symbols available for quotation from @source. 1629 func (env *Env) GetForeignSymbols(c *gin.Context) { 1630 if !validateInputParams(c) { 1631 return 1632 } 1633 1634 source := c.Param("source") 1635 1636 q, err := env.DataStore.GetForeignSymbolsInflux(source) 1637 if err != nil { 1638 if errors.Is(err, redis.Nil) { 1639 restApi.SendError(c, http.StatusNotFound, err) 1640 } else { 1641 restApi.SendError(c, http.StatusInternalServerError, err) 1642 } 1643 } else { 1644 c.JSON(http.StatusOK, q) 1645 } 1646 1647 } 1648 1649 // ----------------------------------------------------------------------------- 1650 // CUSTOMIZED PRODUCTS 1651 // ----------------------------------------------------------------------------- 1652 1653 func (env *Env) GetVwapFirefly(c *gin.Context) { 1654 if !validateInputParams(c) { 1655 return 1656 } 1657 1658 foreignname := c.Param("ticker") 1659 starttimeStr := c.Query("starttime") 1660 endtimeStr := c.Query("endtime") 1661 1662 var starttime, endtime time.Time 1663 if starttimeStr == "" || endtimeStr == "" { 1664 starttime = time.Now().Add(time.Duration(-4 * time.Hour)) 1665 endtime = time.Now() 1666 } else { 1667 starttimeInt, err := strconv.ParseInt(starttimeStr, 10, 64) 1668 if err != nil { 1669 restApi.SendError(c, http.StatusInternalServerError, err) 1670 return 1671 } 1672 starttime = time.Unix(starttimeInt, 0) 1673 endtimeInt, err := strconv.ParseInt(endtimeStr, 10, 64) 1674 if err != nil { 1675 restApi.SendError(c, http.StatusInternalServerError, err) 1676 return 1677 } 1678 endtime = time.Unix(endtimeInt, 0) 1679 if ok := utils.ValidTimeRange(starttime, endtime, time.Duration(30*24*time.Hour)); !ok { 1680 restApi.SendError(c, http.StatusInternalServerError, fmt.Errorf("time-range too big. max duration is %v", 30*24*time.Hour)) 1681 return 1682 } 1683 } 1684 1685 type vwapObj struct { 1686 Ticker string 1687 Value float64 1688 Timestamp time.Time 1689 } 1690 values, timestamps, err := env.DataStore.GetVWAPFirefly(foreignname, starttime, endtime) 1691 if err != nil { 1692 restApi.SendError(c, http.StatusInternalServerError, err) 1693 return 1694 } 1695 if starttimeStr == "" || endtimeStr == "" { 1696 response := vwapObj{ 1697 Ticker: foreignname, 1698 Value: values[0], 1699 Timestamp: timestamps[0], 1700 } 1701 c.JSON(http.StatusOK, response) 1702 } else { 1703 var response []vwapObj 1704 for i := 0; i < len(values); i++ { 1705 tmp := vwapObj{ 1706 Ticker: foreignname, 1707 Value: values[i], 1708 Timestamp: timestamps[i], 1709 } 1710 response = append(response, tmp) 1711 } 1712 c.JSON(http.StatusOK, response) 1713 } 1714 } 1715 1716 func (env *Env) GetLastTradeTime(c *gin.Context) { 1717 if !validateInputParams(c) { 1718 return 1719 } 1720 1721 exchange := c.Param("exchange") 1722 blockchain := c.Param("blockchain") 1723 address := normalizeAddress(c.Param("address"), blockchain) 1724 1725 t, err := env.DataStore.GetLastTradeTimeForExchange(dia.Asset{Address: address, Blockchain: blockchain}, exchange) 1726 if err != nil { 1727 if errors.Is(err, redis.Nil) { 1728 restApi.SendError(c, http.StatusNotFound, err) 1729 } else { 1730 restApi.SendError(c, http.StatusInternalServerError, err) 1731 } 1732 } else { 1733 c.JSON(http.StatusOK, *t) 1734 } 1735 } 1736 1737 // GetLastTrades returns last N trades of an asset. Defaults to N=1000. 1738 func (env *Env) GetLastTradesAsset(c *gin.Context) { 1739 if !validateInputParams(c) { 1740 return 1741 } 1742 1743 blockchain := c.Param("blockchain") 1744 address := normalizeAddress(c.Param("address"), blockchain) 1745 1746 numTradesString := c.DefaultQuery("numTrades", "1000") 1747 exchange := c.Query("exchange") 1748 1749 var numTrades int64 1750 var err error 1751 numTrades, err = strconv.ParseInt(numTradesString, 10, 64) 1752 if err != nil { 1753 numTrades = 1000 1754 } 1755 if numTrades > 5000 { 1756 numTrades = 5000 1757 } 1758 1759 asset, err := env.RelDB.GetAsset(address, blockchain) 1760 if err != nil { 1761 restApi.SendError(c, http.StatusNotFound, err) 1762 return 1763 } 1764 1765 q, err := env.DataStore.GetLastTrades(asset, exchange, time.Now(), int(numTrades), true) 1766 if err != nil { 1767 if errors.Is(err, redis.Nil) { 1768 restApi.SendError(c, http.StatusNotFound, err) 1769 } else { 1770 restApi.SendError(c, http.StatusInternalServerError, err) 1771 } 1772 } else { 1773 c.JSON(http.StatusOK, q) 1774 } 1775 } 1776 1777 // GetMissingExchangeSymbol returns all unverified symbol 1778 func (env *Env) GetMissingExchangeSymbol(c *gin.Context) { 1779 if !validateInputParams(c) { 1780 return 1781 } 1782 1783 exchange := c.Param("exchange") 1784 1785 //symbols, err := api.GetUnverifiedExchangeSymbols(exchange) 1786 symbols, err := env.RelDB.GetUnverifiedExchangeSymbols(exchange) 1787 if err != nil { 1788 restApi.SendError(c, http.StatusInternalServerError, err) 1789 } else { 1790 c.JSON(http.StatusOK, symbols) 1791 } 1792 } 1793 1794 func (env *Env) GetAsset(c *gin.Context) { 1795 if !validateInputParams(c) { 1796 return 1797 } 1798 1799 symbol := c.Param("symbol") 1800 1801 symbols, err := env.RelDB.GetAssets(symbol) 1802 if err != nil { 1803 restApi.SendError(c, http.StatusInternalServerError, err) 1804 } else { 1805 c.JSON(http.StatusOK, symbols) 1806 } 1807 } 1808 1809 func (env *Env) GetAssetExchanges(c *gin.Context) { 1810 if !validateInputParams(c) { 1811 return 1812 } 1813 1814 symbol := c.Param("symbol") 1815 1816 symbols, err := env.RelDB.GetAssetExchange(symbol) 1817 if err != nil { 1818 restApi.SendError(c, http.StatusInternalServerError, err) 1819 } else { 1820 c.JSON(http.StatusOK, symbols) 1821 } 1822 } 1823 1824 func (env *Env) GetAllBlockchains(c *gin.Context) { 1825 blockchains, err := env.RelDB.GetAllAssetsBlockchains() 1826 if err != nil { 1827 restApi.SendError(c, http.StatusInternalServerError, err) 1828 } else { 1829 c.JSON(http.StatusOK, blockchains) 1830 } 1831 } 1832 1833 func (env *Env) GetFeedStats(c *gin.Context) { 1834 if !validateInputParams(c) { 1835 return 1836 } 1837 1838 // Return type for the trades distribution statistics. 1839 type localDistType struct { 1840 NumTradesTotal int64 `json:"NumTradesTotal"` 1841 NumBins int `json:"NumBins"` 1842 NumLowBins int `json:"NumberLowBins"` 1843 Threshold int `json:"Threshold"` 1844 SizeBinSeconds int64 `json:"SizeBinSeconds"` 1845 AvgNumPerBin float64 `json:"AverageNumberPerBin"` 1846 StdDeviation float64 `json:"StandardDeviation"` 1847 TimeRangeSeconds int64 `json:"TimeRangeSeconds"` 1848 } 1849 1850 // Return type for pair volumes per exchange. 1851 type exchangeVolumes struct { 1852 Exchange string 1853 PairVolumes []dia.PairVolume 1854 } 1855 1856 // Overall return type. 1857 type localReturn struct { 1858 Timestamp time.Time 1859 TotalVolume float64 1860 Price float64 1861 TradesDistribution localDistType 1862 ExchangeVolumes []exchangeVolumes 1863 } 1864 1865 // ---- Parse / check input ---- 1866 1867 blockchain := c.Param("blockchain") 1868 address := normalizeAddress(c.Param("address"), blockchain) 1869 1870 // Make starttime and endtime from Unix time input strings. 1871 starttime, endtime, err := utils.MakeTimerange(c.Query("starttime"), c.Query("endtime"), time.Duration(24*time.Hour)) 1872 if err != nil { 1873 log.Error("make timerange: ", err) 1874 } 1875 1876 // Check whether time range is feasible. 1877 if starttime.After(endtime) { 1878 restApi.SendError(c, http.StatusNotAcceptable, fmt.Errorf("endtime must be later than starttime")) 1879 return 1880 } 1881 if ok := utils.ValidTimeRange(starttime, endtime, time.Duration(24*time.Hour)); !ok { 1882 restApi.SendError(c, http.StatusInternalServerError, fmt.Errorf("time-range too big. max duration is %v", 24*time.Hour)) 1883 return 1884 } 1885 1886 tradesBinThreshold, err := strconv.Atoi(c.DefaultQuery("tradesThreshold", "2")) 1887 if err != nil { 1888 log.Warn("parse trades bin threshold: ", err) 1889 tradesBinThreshold = 2 1890 } 1891 1892 sizeBinSeconds, err := strconv.Atoi(c.DefaultQuery("sizeBinSeconds", "120")) 1893 if err != nil { 1894 log.Warn("parse sizeBinSeconds: ", err) 1895 sizeBinSeconds = 120 1896 } 1897 1898 volumeThreshold, err := strconv.ParseFloat(c.DefaultQuery("volumeThreshold", "0"), 64) 1899 if err != nil { 1900 log.Warn("parse volumeThreshold: ", err) 1901 } 1902 1903 if sizeBinSeconds < 20 || sizeBinSeconds > 21600 { 1904 restApi.SendError( 1905 c, 1906 http.StatusInternalServerError, 1907 fmt.Errorf("sizeBinSeconds out of range. Must be between %v and %v", 20*time.Second, 6*time.Hour), 1908 ) 1909 return 1910 } 1911 1912 // ---- Get data for input ---- 1913 1914 asset := env.getAssetFromCache(ASSET_CACHE, blockchain, address) 1915 if err != nil { 1916 restApi.SendError(c, http.StatusInternalServerError, nil) 1917 } 1918 1919 volumeMap, err := env.DataStore.GetExchangePairVolumes(asset, starttime, endtime, volumeThreshold) 1920 if err != nil { 1921 restApi.SendError(c, http.StatusInternalServerError, nil) 1922 return 1923 } 1924 1925 numTradesSeries, err := env.DataStore.GetNumTradesSeries(asset, "", starttime, endtime, strconv.Itoa(sizeBinSeconds)+"s") 1926 if err != nil { 1927 log.Error("get number of trades' series: ", err) 1928 } 1929 1930 // ---- Fill return types with fetched data ----- 1931 1932 var ( 1933 result localReturn 1934 // tradesDistribution localDistType 1935 ev []exchangeVolumes 1936 ) 1937 1938 for key, value := range volumeMap { 1939 1940 var e exchangeVolumes 1941 e.Exchange = key 1942 e.PairVolumes = value 1943 1944 // Collect total volume and full asset information. 1945 for i, v := range value { 1946 result.TotalVolume += v.Volume 1947 e.PairVolumes[i].Pair.QuoteToken = env.getAssetFromCache(ASSET_CACHE, blockchain, address) 1948 e.PairVolumes[i].Pair.BaseToken = env.getAssetFromCache(ASSET_CACHE, v.Pair.BaseToken.Blockchain, v.Pair.BaseToken.Address) 1949 } 1950 1951 // Sort pairs per exchange by volume. 1952 aux := value 1953 sort.Slice(aux, func(k, l int) bool { 1954 return aux[k].Volume > aux[l].Volume 1955 }) 1956 ev = append(ev, e) 1957 1958 // Sort exchanges by volume. 1959 sort.Slice(ev, func(k, l int) bool { 1960 var ExchangeSums []float64 1961 for _, exchange := range ev { 1962 var S float64 1963 for _, vol := range exchange.PairVolumes { 1964 S += vol.Volume 1965 } 1966 ExchangeSums = append(ExchangeSums, S) 1967 } 1968 return ExchangeSums[k] > ExchangeSums[l] 1969 }) 1970 1971 } 1972 1973 result.ExchangeVolumes = ev 1974 result.Timestamp = endtime 1975 result.Price, err = env.DataStore.GetAssetPriceUSD(asset, endtime.Add(-time.Duration(ASSETQUOTATION_LOOKBACK_HOURS)*time.Hour), endtime) 1976 if err != nil { 1977 log.Warn("get price for asset: ", err) 1978 } 1979 1980 // Trades Distribution values. 1981 result.TradesDistribution.Threshold = tradesBinThreshold 1982 result.TradesDistribution.NumBins = len(numTradesSeries) 1983 result.TradesDistribution.SizeBinSeconds = int64(sizeBinSeconds) 1984 var numTradesSeriesFloat []float64 1985 for _, num := range numTradesSeries { 1986 numTradesSeriesFloat = append(numTradesSeriesFloat, float64(num)) 1987 result.TradesDistribution.NumTradesTotal += num 1988 if num < int64(tradesBinThreshold) { 1989 result.TradesDistribution.NumLowBins++ 1990 } 1991 } 1992 if len(volumeMap) == 0 { 1993 result.TradesDistribution.NumBins = int(endtime.Sub(starttime).Seconds()) / sizeBinSeconds 1994 result.TradesDistribution.NumLowBins = result.TradesDistribution.NumBins 1995 } 1996 if len(numTradesSeries) > 0 { 1997 result.TradesDistribution.AvgNumPerBin = float64(result.TradesDistribution.NumTradesTotal) / float64(len(numTradesSeries)) 1998 } 1999 result.TradesDistribution.StdDeviation = utils.StandardDeviation(numTradesSeriesFloat) 2000 result.TradesDistribution.TimeRangeSeconds = int64(endtime.Sub(starttime).Seconds()) 2001 2002 c.JSON(http.StatusOK, result) 2003 2004 } 2005 2006 // GetAssetUpdates returns the number of updates an oracle with the given parameters 2007 // would have done in the given time-range. 2008 func (env *Env) GetAssetUpdates(c *gin.Context) { 2009 if !validateInputParams(c) { 2010 return 2011 } 2012 2013 type localDeviationType struct { 2014 Time time.Time `json:"Time"` 2015 Deviation float64 `json:"Deviation"` 2016 } 2017 type localResultType struct { 2018 UpdateCount int `json:"UpdateCount"` 2019 UpdatesPer24h float64 `json:"UpdatesPer24h"` 2020 Asset dia.Asset `json:"Asset"` 2021 Deviations []localDeviationType `json:"Deviations"` 2022 } 2023 2024 blockchain := c.Param("blockchain") 2025 address := normalizeAddress(c.Param("address"), blockchain) 2026 2027 // Deviation in per mille. 2028 deviation, err := strconv.Atoi(c.Param("deviation")) 2029 if err != nil { 2030 restApi.SendError(c, http.StatusInternalServerError, err) 2031 return 2032 } 2033 FrequencySeconds, err := strconv.Atoi(c.Param("frequencySeconds")) 2034 if err != nil { 2035 restApi.SendError(c, http.StatusInternalServerError, err) 2036 return 2037 } 2038 2039 starttime, endtime, err := utils.MakeTimerange(c.Query("starttime"), c.Query("endtime"), time.Duration(24*60)*time.Minute) 2040 if err != nil { 2041 restApi.SendError(c, http.StatusInternalServerError, err) 2042 return 2043 } 2044 if endtime.Sub(starttime) > time.Duration(7*24*60)*time.Minute { 2045 restApi.SendError(c, http.StatusRequestedRangeNotSatisfiable, errors.New("requested time-range too large")) 2046 return 2047 } 2048 if ok := utils.ValidTimeRange(starttime, endtime, 30*24*time.Hour); !ok { 2049 restApi.SendError(c, http.StatusInternalServerError, fmt.Errorf("time-range too big. max duration is %v", 30*24*time.Hour)) 2050 return 2051 } 2052 2053 asset, errGetAsset := env.RelDB.GetAsset(address, blockchain) 2054 if errGetAsset != nil { 2055 restApi.SendError(c, http.StatusInternalServerError, errGetAsset) 2056 return 2057 } 2058 2059 quotations, errGetAssetQuotations := env.DataStore.GetAssetQuotations(asset, starttime, endtime) 2060 if errGetAssetQuotations != nil { 2061 restApi.SendError(c, http.StatusInternalServerError, errGetAssetQuotations) 2062 return 2063 } 2064 2065 var lrt localResultType 2066 2067 lastQuotation := quotations[len(quotations)-1] 2068 lastValue := lastQuotation.Price 2069 for i := len(quotations) - 2; i >= 0; i-- { 2070 var diff float64 2071 // Oracle did not check for new quotation yet. 2072 if quotations[i].Time.Sub(lastQuotation.Time) < time.Duration(FrequencySeconds)*time.Second { 2073 continue 2074 } 2075 if lastValue != 0 { 2076 diff = math.Abs((quotations[i].Price - lastValue) / lastValue) 2077 } else { 2078 // Artificially make diff large enough for update (instead of infty). 2079 diff = float64(deviation)/1000 + 1 2080 } 2081 // If deviation is large enough, update values. 2082 if diff > float64(deviation)/1000 { 2083 lastQuotation = quotations[i] 2084 lastValue = lastQuotation.Price 2085 2086 var ldt localDeviationType 2087 ldt.Deviation = diff 2088 ldt.Time = lastQuotation.Time 2089 lrt.Deviations = append(lrt.Deviations, ldt) 2090 lrt.UpdateCount++ 2091 2092 } 2093 } 2094 2095 lrt.Asset = asset 2096 lrt.UpdatesPer24h = float64(lrt.UpdateCount) * float64(time.Duration(24*time.Hour).Hours()/endtime.Sub(starttime).Hours()) 2097 c.JSON(http.StatusOK, lrt) 2098 } 2099 2100 // GetAssetInfo returns quotation of asset with highest market cap among 2101 // all assets with symbol ticker @symbol. Additionally information on exchanges and volumes. 2102 func (env *Env) GetAssetInfo(c *gin.Context) { 2103 if !validateInputParams(c) { 2104 return 2105 } 2106 type localExchangeInfo struct { 2107 Name string 2108 Volume24h float64 2109 NumPairs int 2110 NumTrades int64 2111 } 2112 2113 type localAssetInfoReturn struct { 2114 Symbol string 2115 Name string 2116 Address string 2117 Blockchain string 2118 Price float64 2119 PriceYesterday float64 2120 VolumeYesterdayUSD float64 2121 Time time.Time 2122 Source string 2123 ExchangeInfo []localExchangeInfo 2124 } 2125 2126 blockchain := c.Param("blockchain") 2127 address := normalizeAddress(c.Param("address"), blockchain) 2128 2129 starttime, endtime, err := utils.MakeTimerange(c.Query("starttime"), c.Query("endtime"), time.Duration(24*60)*time.Minute) 2130 if err != nil { 2131 restApi.SendError(c, http.StatusInternalServerError, err) 2132 return 2133 } 2134 2135 var quotationExtended localAssetInfoReturn 2136 2137 asset, err := env.RelDB.GetAsset(address, blockchain) 2138 if err != nil { 2139 restApi.SendError(c, http.StatusNotFound, err) 2140 return 2141 } 2142 2143 quotation, err := env.DataStore.GetAssetQuotation(asset, endtime.Add(-time.Duration(ASSETQUOTATION_LOOKBACK_HOURS)*time.Hour), endtime) 2144 if err != nil { 2145 restApi.SendError(c, http.StatusNotFound, errors.New("no quotation available")) 2146 return 2147 } 2148 quotationYesterday, err := env.DataStore.GetAssetQuotation(asset, endtime.Add(-time.Duration(ASSETQUOTATION_LOOKBACK_HOURS)*time.Hour), starttime) 2149 if err != nil { 2150 log.Warn("get quotation yesterday: ", err) 2151 } else { 2152 quotationExtended.PriceYesterday = quotationYesterday.Price 2153 } 2154 volumeYesterday, err := env.DataStore.GetVolumeInflux(asset, "", starttime, endtime) 2155 if err != nil { 2156 log.Warn("get volume yesterday: ", err) 2157 } else { 2158 quotationExtended.VolumeYesterdayUSD = *volumeYesterday 2159 } 2160 quotationExtended.Symbol = quotation.Asset.Symbol 2161 quotationExtended.Name = quotation.Asset.Name 2162 quotationExtended.Address = quotation.Asset.Address 2163 quotationExtended.Blockchain = quotation.Asset.Blockchain 2164 quotationExtended.Price = quotation.Price 2165 quotationExtended.Time = quotation.Time 2166 quotationExtended.Source = quotation.Source 2167 2168 // Get Exchange stats 2169 exchangemap, _, err := env.DataStore.GetActiveExchangesAndPairs(asset.Address, asset.Blockchain, int64(0), starttime, endtime) 2170 if err != nil { 2171 restApi.SendError(c, http.StatusNotFound, err) 2172 return 2173 } 2174 var eix []localExchangeInfo 2175 for exchange, pairs := range exchangemap { 2176 var ei localExchangeInfo 2177 ei.Name = exchange 2178 ei.NumPairs = len(pairs) 2179 ei.NumTrades, err = env.DataStore.GetNumTrades(exchange, asset.Address, asset.Blockchain, starttime, endtime) 2180 if err != nil { 2181 log.Errorf("get number of trades for %s: %v", exchange, err) 2182 } 2183 vol, err := env.DataStore.GetVolumeInflux(asset, exchange, starttime, endtime) 2184 if err != nil { 2185 log.Errorf("get 24h volume for %s: %v", exchange, err) 2186 } else { 2187 ei.Volume24h = *vol 2188 } 2189 eix = append(eix, ei) 2190 } 2191 2192 sort.Slice(eix, func(i, j int) bool { 2193 return eix[i].Volume24h > eix[j].Volume24h 2194 }) 2195 quotationExtended.ExchangeInfo = eix 2196 2197 c.JSON(http.StatusOK, quotationExtended) 2198 } 2199 2200 // GetPairsInFeed returns quotation of asset with highest market cap among 2201 // all assets with symbol ticker @symbol. Additionally information on exchanges and volumes. 2202 func (env *Env) GetPairsInFeed(c *gin.Context) { 2203 if !validateInputParams(c) { 2204 return 2205 } 2206 2207 type localPairInfo struct { 2208 ForeignName string 2209 Exchange string 2210 NumTrades int64 2211 Quotetoken dia.Asset 2212 Basetoken dia.Asset 2213 } 2214 2215 type localAssetInfoReturn struct { 2216 Symbol string 2217 Name string 2218 Address string 2219 Blockchain string 2220 Price float64 2221 PriceYesterday float64 2222 VolumeYesterdayUSD float64 2223 Time time.Time 2224 Source string 2225 PairInfo []localPairInfo 2226 } 2227 var quotationExtended localAssetInfoReturn 2228 2229 blockchain := c.Param("blockchain") 2230 address := normalizeAddress(c.Param("address"), blockchain) 2231 numTradesThreshold, err := strconv.ParseInt(c.Param("numTradesThreshold"), 10, 64) 2232 if err != nil { 2233 restApi.SendError(c, http.StatusInternalServerError, err) 2234 return 2235 } 2236 2237 starttime, endtime, err := utils.MakeTimerange(c.Query("starttime"), c.Query("endtime"), time.Duration(24*60)*time.Minute) 2238 if err != nil { 2239 restApi.SendError(c, http.StatusInternalServerError, err) 2240 return 2241 } 2242 2243 asset := env.getAssetFromCache(ASSET_CACHE, blockchain, address) 2244 2245 quotation, err := env.DataStore.GetAssetQuotation(asset, dia.CRYPTO_ZERO_UNIX_TIME, endtime) 2246 if err != nil { 2247 restApi.SendError(c, http.StatusNotFound, errors.New("no quotation available")) 2248 return 2249 } 2250 quotationYesterday, err := env.DataStore.GetAssetQuotation(asset, dia.CRYPTO_ZERO_UNIX_TIME, starttime) 2251 if err != nil { 2252 log.Warn("get quotation yesterday: ", err) 2253 } else { 2254 quotationExtended.PriceYesterday = quotationYesterday.Price 2255 } 2256 volumeYesterday, err := env.DataStore.Get24HoursAssetVolume(asset) 2257 if err != nil { 2258 log.Warn("get volume yesterday: ", err) 2259 } else { 2260 quotationExtended.VolumeYesterdayUSD = *volumeYesterday 2261 } 2262 quotationExtended.Symbol = quotation.Asset.Symbol 2263 quotationExtended.Name = quotation.Asset.Name 2264 quotationExtended.Address = quotation.Asset.Address 2265 quotationExtended.Blockchain = quotation.Asset.Blockchain 2266 quotationExtended.Price = quotation.Price 2267 quotationExtended.Time = quotation.Time 2268 quotationExtended.Source = quotation.Source 2269 2270 // Get Exchange stats 2271 exchangemap, pairCountMap, err := env.DataStore.GetActiveExchangesAndPairs(asset.Address, asset.Blockchain, numTradesThreshold, starttime, endtime) 2272 if err != nil { 2273 restApi.SendError(c, http.StatusNotFound, err) 2274 return 2275 } 2276 2277 var eix []localPairInfo 2278 for exchange, pairs := range exchangemap { 2279 var ei localPairInfo 2280 ei.Exchange = exchange 2281 2282 for _, pair := range pairs { 2283 ei.NumTrades = pairCountMap[pair.PairExchangeIdentifier(exchange)] 2284 ei.Quotetoken = asset 2285 ei.Basetoken = env.getAssetFromCache(ASSET_CACHE, pair.BaseToken.Blockchain, pair.BaseToken.Address) 2286 ei.ForeignName = ei.Quotetoken.Symbol + "-" + ei.Basetoken.Symbol 2287 eix = append(eix, ei) 2288 } 2289 2290 } 2291 2292 sort.Slice(eix, func(i, j int) bool { 2293 return eix[i].NumTrades > eix[j].NumTrades 2294 }) 2295 quotationExtended.PairInfo = eix 2296 2297 c.JSON(http.StatusOK, quotationExtended) 2298 } 2299 2300 func (env *Env) GetAvailableAssets(c *gin.Context) { 2301 if !validateInputParams(c) { 2302 return 2303 } 2304 2305 assetClass := c.Param("assetClass") 2306 2307 if assetClass == "CryptoToken" { 2308 assets, err := env.RelDB.GetAllExchangeAssets(true) 2309 if err != nil { 2310 restApi.SendError(c, http.StatusInternalServerError, err) 2311 return 2312 } 2313 c.JSON(http.StatusOK, assets) 2314 } else { 2315 restApi.SendError(c, http.StatusInternalServerError, errors.New("unknown asset class")) 2316 return 2317 } 2318 } 2319 2320 func validateInputParams(c *gin.Context) bool { 2321 2322 // Validate input parameters. 2323 for _, input := range c.Params { 2324 if containsSpecialChars(input.Value) { 2325 restApi.SendError(c, http.StatusInternalServerError, errors.New("invalid input params")) 2326 return false 2327 } 2328 } 2329 2330 // Validate query parameters. 2331 for _, value := range c.Request.URL.Query() { 2332 for _, input := range value { 2333 if containsSpecialChars(input) { 2334 restApi.SendError(c, http.StatusInternalServerError, errors.New("invalid input params")) 2335 return false 2336 } 2337 } 2338 } 2339 2340 return true 2341 } 2342 2343 func containsSpecialChars(s string) bool { 2344 return strings.ContainsAny(s, "!@#$%^&*()'\"|{}[];><?/`~,") 2345 } 2346 2347 // Returns the EIP55 compliant address in case @blockchain has an Ethereum ChainID. 2348 func makeAddressEIP55Compliant(address string, blockchain string) string { 2349 if strings.Contains(BLOCKCHAINS[blockchain].ChainID, "Ethereum") { 2350 return common.HexToAddress(address).Hex() 2351 } 2352 return address 2353 } 2354 2355 // Normalize address depending on the blockchain. 2356 func normalizeAddress(address string, blockchain string) string { 2357 if strings.Contains(BLOCKCHAINS[blockchain].ChainID, "Ethereum") { 2358 return makeAddressEIP55Compliant(address, blockchain) 2359 } 2360 if BLOCKCHAINS[blockchain].Name == dia.OSMOSIS { 2361 if strings.Contains(address, "ibc-") && len(strings.Split(address, "-")[1]) > 1 { 2362 return "ibc/" + strings.Split(address, "-")[1] 2363 } 2364 } 2365 return address 2366 } 2367 2368 // getDecimalsFromCache returns the decimals of @asset, either from the map @localCache or from 2369 // Postgres, in which latter case it also adds the decimals to the local cache. 2370 // Remember that maps are always passed by reference. 2371 func (env *Env) getDecimalsFromCache(localCache map[dia.Asset]uint8, asset dia.Asset) uint8 { 2372 if decimals, ok := localCache[asset]; ok { 2373 return decimals 2374 } 2375 fullAsset, err := env.RelDB.GetAsset(asset.Address, asset.Blockchain) 2376 if err != nil { 2377 log.Warnf("could not find asset with address %s on blockchain %s in postgres: ", asset.Address, asset.Blockchain) 2378 } 2379 localCache[asset] = fullAsset.Decimals 2380 return fullAsset.Decimals 2381 } 2382 2383 // getAssetFromCache returns the full asset given by blockchain and address, either from the map @localCache 2384 // or from Postgres, in which latter case it also adds the asset to the local cache. 2385 // Remember that maps are always passed by reference. 2386 func (env *Env) getAssetFromCache(localCache map[string]dia.Asset, blockchain string, address string) dia.Asset { 2387 if asset, ok := localCache[assetIdentifier(blockchain, address)]; ok { 2388 return asset 2389 } 2390 fullAsset, err := env.RelDB.GetAsset(address, blockchain) 2391 if err != nil { 2392 log.Warnf("could not find asset with address %s on blockchain %s in postgres: ", address, blockchain) 2393 } 2394 localCache[assetIdentifier(blockchain, address)] = fullAsset 2395 return fullAsset 2396 } 2397 2398 func assetIdentifier(blockchain string, address string) string { 2399 return blockchain + "-" + address 2400 } 2401 2402 func (env *Env) SearchAssetList(c *gin.Context) { 2403 if !validateInputParams(c) { 2404 return 2405 } 2406 2407 querystring := c.Param("query") 2408 var ( 2409 assets = []dia.AssetList{} 2410 err error 2411 ) 2412 2413 assets, err = env.RelDB.SearchAssetList(querystring) 2414 if err != nil { 2415 // restApi.SendError(c, http.StatusInternalServerError, errors.New("eror getting asset")) 2416 log.Errorln("error getting SearchAssetList", err) 2417 } 2418 2419 c.JSON(http.StatusOK, assets) 2420 } 2421 2422 func (env *Env) GetAssetList(c *gin.Context) { 2423 if !validateInputParams(c) { 2424 return 2425 } 2426 listname := c.Param("listname") 2427 2428 assets, err := env.RelDB.GetAssetList(listname) 2429 if err != nil { 2430 // restApi.SendError(c, http.StatusInternalServerError, errors.New("eror getting asset")) 2431 log.Errorln("error getting GetAssetList", err) 2432 } 2433 if len(assets) <= 0 { 2434 restApi.SendError(c, http.StatusNotFound, errors.New("asset missing")) 2435 return 2436 } 2437 2438 c.JSON(http.StatusOK, assets) 2439 } 2440 2441 func (env *Env) GetAssetListBySymbol(c *gin.Context) { 2442 if !validateInputParams(c) { 2443 return 2444 } 2445 listname := c.Param("listname") 2446 2447 querystring := c.Param("symbol") 2448 querystring = strings.ToUpper(querystring) 2449 var ( 2450 assets = []dia.AssetList{} 2451 err error 2452 ) 2453 2454 assets, err = env.RelDB.GetAssetListBySymbol(querystring, listname) 2455 if err != nil { 2456 // restApi.SendError(c, http.StatusInternalServerError, errors.New("eror getting asset")) 2457 log.Errorln("error getting SearchAssetList", err) 2458 } 2459 if len(assets) <= 0 { 2460 restApi.SendError(c, http.StatusNotFound, errors.New("asset missing")) 2461 return 2462 } 2463 2464 selectedAsset := assets[0] 2465 2466 splitted := strings.Split(selectedAsset.AssetName, "-") 2467 2468 price, _, time, source, err := gqlclient.GetGraphqlAssetQuotationFromDia(splitted[0], splitted[1], 60, selectedAsset) 2469 if err != nil { 2470 // restApi.SendError(c, http.StatusInternalServerError, errors.New("eror getting asset")) 2471 log.Errorln("error getting GetGraphqlAssetQuotationFromDia", err) 2472 } 2473 2474 asset := dia.Asset{Symbol: selectedAsset.Symbol, Name: selectedAsset.CustomName, Blockchain: splitted[0], Address: splitted[1]} 2475 q := models.AssetQuotationFull{Symbol: asset.Symbol, Name: asset.Name, Address: asset.Address, Price: price, Blockchain: asset.Blockchain} 2476 2477 volumeYesterday, err := env.DataStore.Get24HoursAssetVolume(asset) 2478 if err != nil { 2479 log.Errorln("error getting Get24HoursAssetVolume", err) 2480 } 2481 q.VolumeYesterdayUSD = *volumeYesterday 2482 q.Time = time 2483 q.Source = strings.Join(source, ",") 2484 c.JSON(http.StatusOK, q) 2485 }