Add Chain 138 wallet network metadata and stats coin-price enrichment; sync frontend explorer SPA, command center, and address/token pages with backend config. Co-authored-by: Cursor <cursoragent@cursor.com>
151 lines
3.4 KiB
Go
151 lines
3.4 KiB
Go
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)
|
|
}
|