mirror of
https://github.com/luxfi/price.git
synced 2026-10-30 15:21:24 +00:00
Lux Partners Limited is not the entity behind this work. Every place the old name stood — copyright lines, site footers, brand config, legal and compliance text — now names the company that actually holds it. lux.fund is the exception and is not renamed to match: that site belongs to Lux Assets Management LLC, a different company, and it says so. Two kinds of line were left for a person rather than rewritten. A dated news item recording that Lux Partners Limited was established in the Isle of Man is a historical fact, and renaming it would falsify the record. And a sentence asserting that the company is domiciled in the Isle of Man is a claim about a jurisdiction, not a name — an Inc or an LLC is not an Isle of Man company, so swapping the name there would assert something untrue. Co-authored-by: Hanzo Dev <[email protected]>
114 lines
3.2 KiB
Go
114 lines
3.2 KiB
Go
// Copyright (c) 2025 Lux Industries Inc
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package source
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// coingeckoDefaultURL is the public/demo CoinGecko endpoint.
|
|
// Pro users provide a key; we still keep using the same base path because
|
|
// the demo and pro paths differ only by host header in the demo plan.
|
|
const coingeckoDefaultURL = "https://api.coingecko.com/api/v3"
|
|
|
|
// CoinGecko fetches quotes from the CoinGecko /coins/markets endpoint.
|
|
//
|
|
// CoinGecko uses canonical IDs ("bitcoin", "ethereum", ...) which match the
|
|
// IDs we accept on the read endpoints, so no symbol mapping is required.
|
|
type CoinGecko struct {
|
|
apiKey string
|
|
baseURL string
|
|
client *http.Client
|
|
enabled bool
|
|
}
|
|
|
|
// NewCoinGecko constructs a CoinGecko source. An empty apiKey falls back to
|
|
// the public demo endpoint with its (low) anonymous rate limit. Pass a
|
|
// custom baseURL ("") to use the default public host.
|
|
func NewCoinGecko(apiKey, baseURL string) *CoinGecko {
|
|
if baseURL == "" {
|
|
baseURL = coingeckoDefaultURL
|
|
}
|
|
return &CoinGecko{
|
|
apiKey: apiKey,
|
|
baseURL: baseURL,
|
|
client: &http.Client{Timeout: 30 * time.Second},
|
|
enabled: true,
|
|
}
|
|
}
|
|
|
|
// Name returns the canonical source identifier.
|
|
func (c *CoinGecko) Name() string { return "coingecko" }
|
|
|
|
// Enabled reports whether this source is wired up for the fetcher.
|
|
func (c *CoinGecko) Enabled() bool { return c != nil && c.enabled }
|
|
|
|
// Fetch returns quotes keyed by token ID. CoinGecko accepts the full set of
|
|
// requested IDs in a single batch call, so we make exactly one request.
|
|
func (c *CoinGecko) Fetch(ctx context.Context, ids []string) (map[string]Quote, error) {
|
|
if !c.Enabled() || len(ids) == 0 {
|
|
return map[string]Quote{}, nil
|
|
}
|
|
|
|
url := fmt.Sprintf(
|
|
"%s/coins/markets?vs_currency=usd&ids=%s&order=market_cap_desc&per_page=250&page=1&sparkline=false",
|
|
c.baseURL,
|
|
strings.Join(ids, ","),
|
|
)
|
|
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if c.apiKey != "" {
|
|
req.Header.Set("x-cg-demo-api-key", c.apiKey)
|
|
}
|
|
req.Header.Set("Accept", "application/json")
|
|
|
|
resp, err := c.client.Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
body, _ := io.ReadAll(resp.Body)
|
|
return nil, fmt.Errorf("coingecko: status %d: %s", resp.StatusCode, string(body))
|
|
}
|
|
|
|
var raw []struct {
|
|
ID string `json:"id"`
|
|
Symbol string `json:"symbol"`
|
|
CurrentPrice float64 `json:"current_price"`
|
|
MarketCap float64 `json:"market_cap"`
|
|
TotalVolume float64 `json:"total_volume"`
|
|
PriceChangePercentage24h float64 `json:"price_change_percentage_24h"`
|
|
}
|
|
if err := json.NewDecoder(resp.Body).Decode(&raw); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
now := time.Now().UTC()
|
|
out := make(map[string]Quote, len(raw))
|
|
for _, r := range raw {
|
|
out[r.ID] = Quote{
|
|
ID: r.ID,
|
|
Symbol: r.Symbol,
|
|
PriceUSD: r.CurrentPrice,
|
|
Change24h: r.PriceChangePercentage24h,
|
|
Volume24h: r.TotalVolume,
|
|
MarketCap: r.MarketCap,
|
|
UpdatedAt: now,
|
|
SourceName: c.Name(),
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|