Files
zeekayandHanzo Dev 1db9b4d974 the corporate name is Lux Industries Inc
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]>
2026-08-28 11:05:38 -07:00

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
}