package rest import ( "context" "encoding/json" "fmt" "io" "net/http" "os" "strings" "time" ) const ( chain138WETHAddress = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" ) type blockscoutCoinStats struct { CoinPrice *string `json:"coin_price"` CoinImage *string `json:"coin_image"` } type tokenAggregationTokenResponse struct { Token struct { Market struct { PriceUsd *float64 `json:"priceUsd"` } `json:"market"` } `json:"token"` } func enrichNativeCoinMarket(ctx context.Context, stats *explorerStats) { if stats.CoinPrice != nil && strings.TrimSpace(*stats.CoinPrice) != "" { return } // Prefer on-chain token-aggregation WETH/ETH oracle over Blockscout CoinGecko on Chain 138. if price := fetchTokenAggregationEthUsd(ctx); price != "" { stats.CoinPrice = &price if image := fetchBlockscoutCoinImage(ctx); image != "" { stats.CoinImage = &image } return } bsURL := strings.TrimSpace(os.Getenv("BLOCKSCOUT_STATS_URL")) if bsURL == "" { bsURL = "http://127.0.0.1:4000/api/v2/stats" } if price, image := fetchBlockscoutCoinMarket(ctx, bsURL); price != "" { stats.CoinPrice = &price if image != "" { stats.CoinImage = &image } } } func fetchBlockscoutCoinImage(ctx context.Context) string { bsURL := strings.TrimSpace(os.Getenv("BLOCKSCOUT_STATS_URL")) if bsURL == "" { bsURL = "http://127.0.0.1:4000/api/v2/stats" } _, image := fetchBlockscoutCoinMarket(ctx, bsURL) return image } func fetchBlockscoutCoinMarket(ctx context.Context, url string) (price string, image string) { req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { return "", "" } client := &http.Client{Timeout: 4 * time.Second} resp, err := client.Do(req) if err != nil { return "", "" } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return "", "" } body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20)) if err != nil { return "", "" } var payload blockscoutCoinStats if err := json.Unmarshal(body, &payload); err != nil { return "", "" } if payload.CoinPrice != nil { price = strings.TrimSpace(*payload.CoinPrice) } if payload.CoinImage != nil { image = strings.TrimSpace(*payload.CoinImage) } return price, image } func tokenAggregationAPIV1Base() string { base := strings.TrimRight(strings.TrimSpace(firstNonEmptyEnv( "TOKEN_AGGREGATION_API_BASE", "TOKEN_AGGREGATION_URL", "TOKEN_AGGREGATION_BASE_URL", )), "/") if base == "" { return "http://127.0.0.1:3001/api/v1" } if strings.HasSuffix(base, "/api/v1") { return base } return base + "/api/v1" } func fetchTokenAggregationEthUsd(ctx context.Context) string { url := fmt.Sprintf("%s/tokens/%s?chainId=138", tokenAggregationAPIV1Base(), chain138WETHAddress) req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { return "" } client := &http.Client{Timeout: 5 * time.Second} resp, err := client.Do(req) if err != nil { return "" } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return "" } body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20)) if err != nil { return "" } var payload tokenAggregationTokenResponse if err := json.Unmarshal(body, &payload); err != nil { return "" } if payload.Token.Market.PriceUsd == nil || *payload.Token.Market.PriceUsd <= 0 { return "" } return formatUsdPrice(*payload.Token.Market.PriceUsd) } func formatUsdPrice(value float64) string { return fmt.Sprintf("%.2f", value) }