mirror of
https://github.com/luxfi/bridge.git
synced 2026-09-24 00:26:46 +00:00
feat: lux rice from live coingecko
This commit is contained in:
@@ -0,0 +1,270 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// coingecko_price_feed.go: HTTP PriceFeed backed by CoinGecko's
|
||||
// simple/price endpoint. Drop-in replacement for StaticPriceFeed.
|
||||
//
|
||||
// Behavior:
|
||||
// - Symbol → CoinGecko ID translation via IDMap (e.g. "ETH" → "ethereum").
|
||||
// Symbols not in IDMap return ErrPriceUnknown (so a FallbackFeed can
|
||||
// route them to the static feed — useful for LUX/ZOO which CoinGecko
|
||||
// doesn't list).
|
||||
// - One batched HTTP call per TTL window covers every configured symbol.
|
||||
// Within the window, Price() reads the cache without hitting the
|
||||
// network. Default TTL: 30s, well inside CoinGecko's free-tier limits.
|
||||
// - On HTTP error (network down, 429, 5xx), all calls return the
|
||||
// underlying error. Wrap with FallbackFeed to keep the bridge live
|
||||
// during CoinGecko outages.
|
||||
|
||||
// =============================================================================
|
||||
// CoinGeckoFeed
|
||||
// =============================================================================
|
||||
|
||||
// DefaultCoinGeckoBaseURL is the public simple/price endpoint root.
|
||||
const DefaultCoinGeckoBaseURL = "https://api.coingecko.com/api/v3"
|
||||
|
||||
// DefaultCoinGeckoCacheTTL is the freshness window for cached prices.
|
||||
const DefaultCoinGeckoCacheTTL = 30 * time.Second
|
||||
|
||||
// DefaultCoinGeckoTimeout is the per-request HTTP timeout.
|
||||
const DefaultCoinGeckoTimeout = 5 * time.Second
|
||||
|
||||
// DefaultCoinGeckoIDMap is the symbol → CoinGecko ID map seeded for the
|
||||
// assets the bridge supports today. LUX and ZOO are deliberately absent;
|
||||
// CoinGecko does not list them, so they must come from a fallback feed.
|
||||
var DefaultCoinGeckoIDMap = map[string]string{
|
||||
"ETH": "ethereum",
|
||||
"BTC": "bitcoin",
|
||||
"SOL": "solana",
|
||||
"TON": "the-open-network",
|
||||
"USDC": "usd-coin",
|
||||
"USDT": "tether",
|
||||
"DAI": "dai",
|
||||
"BNB": "binancecoin",
|
||||
}
|
||||
|
||||
// CoinGeckoFeed pulls USD spot prices from CoinGecko's simple/price
|
||||
// endpoint. Concurrency-safe.
|
||||
type CoinGeckoFeed struct {
|
||||
// BaseURL is the API root, default DefaultCoinGeckoBaseURL.
|
||||
// Tests override this with httptest.NewServer.URL.
|
||||
BaseURL string
|
||||
// APIKey is the CoinGecko Pro key (x-cg-pro-api-key header). Empty
|
||||
// for the free tier; rate limits apply.
|
||||
APIKey string
|
||||
// HTTPClient is the transport. Defaults to a client with
|
||||
// DefaultCoinGeckoTimeout. Tests can swap in a custom client.
|
||||
HTTPClient *http.Client
|
||||
// CacheTTL is the freshness window. Zero ⇒ DefaultCoinGeckoCacheTTL.
|
||||
CacheTTL time.Duration
|
||||
// IDMap is the symbol → CoinGecko ID lookup. Zero-value ⇒
|
||||
// DefaultCoinGeckoIDMap. Symbol matching is case-insensitive.
|
||||
IDMap map[string]string
|
||||
|
||||
mu sync.Mutex
|
||||
cache map[string]float64 // upper-case symbol → USD
|
||||
cachedAt time.Time
|
||||
inFlight chan struct{} // single-flight gate around HTTP fetch
|
||||
}
|
||||
|
||||
// NewCoinGeckoFeed builds a feed with sensible defaults. Pass nil for
|
||||
// idMap to use DefaultCoinGeckoIDMap.
|
||||
func NewCoinGeckoFeed(baseURL, apiKey string, idMap map[string]string) *CoinGeckoFeed {
|
||||
if baseURL == "" {
|
||||
baseURL = DefaultCoinGeckoBaseURL
|
||||
}
|
||||
if idMap == nil {
|
||||
idMap = DefaultCoinGeckoIDMap
|
||||
}
|
||||
upper := make(map[string]string, len(idMap))
|
||||
for k, v := range idMap {
|
||||
upper[strings.ToUpper(k)] = v
|
||||
}
|
||||
return &CoinGeckoFeed{
|
||||
BaseURL: baseURL,
|
||||
APIKey: apiKey,
|
||||
HTTPClient: &http.Client{Timeout: DefaultCoinGeckoTimeout},
|
||||
IDMap: upper,
|
||||
}
|
||||
}
|
||||
|
||||
// Price returns the USD value of one unit of asset.
|
||||
func (f *CoinGeckoFeed) Price(ctx context.Context, asset string) (float64, error) {
|
||||
sym := strings.ToUpper(asset)
|
||||
if _, ok := f.IDMap[sym]; !ok {
|
||||
return 0, fmt.Errorf("%w: %s (not in CoinGecko ID map)", ErrPriceUnknown, asset)
|
||||
}
|
||||
|
||||
if v, ok := f.cachedFresh(sym); ok {
|
||||
return v, nil
|
||||
}
|
||||
|
||||
if err := f.refresh(ctx); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if v, ok := f.cachedFresh(sym); ok {
|
||||
return v, nil
|
||||
}
|
||||
return 0, fmt.Errorf("%w: %s (CoinGecko response missing this id)", ErrPriceUnknown, asset)
|
||||
}
|
||||
|
||||
func (f *CoinGeckoFeed) cachedFresh(sym string) (float64, bool) {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
if f.cache == nil || time.Since(f.cachedAt) > f.ttl() {
|
||||
return 0, false
|
||||
}
|
||||
v, ok := f.cache[sym]
|
||||
return v, ok
|
||||
}
|
||||
|
||||
func (f *CoinGeckoFeed) ttl() time.Duration {
|
||||
if f.CacheTTL > 0 {
|
||||
return f.CacheTTL
|
||||
}
|
||||
return DefaultCoinGeckoCacheTTL
|
||||
}
|
||||
|
||||
// refresh fetches every configured symbol's USD price in one HTTP call
|
||||
// and replaces the cache. Single-flight: concurrent refreshes coalesce
|
||||
// onto whichever goroutine got there first.
|
||||
func (f *CoinGeckoFeed) refresh(ctx context.Context) error {
|
||||
f.mu.Lock()
|
||||
if f.inFlight != nil {
|
||||
ch := f.inFlight
|
||||
f.mu.Unlock()
|
||||
select {
|
||||
case <-ch:
|
||||
return nil
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
}
|
||||
ch := make(chan struct{})
|
||||
f.inFlight = ch
|
||||
f.mu.Unlock()
|
||||
|
||||
defer func() {
|
||||
f.mu.Lock()
|
||||
f.inFlight = nil
|
||||
close(ch)
|
||||
f.mu.Unlock()
|
||||
}()
|
||||
|
||||
ids := make([]string, 0, len(f.IDMap))
|
||||
for _, v := range f.IDMap {
|
||||
ids = append(ids, v)
|
||||
}
|
||||
sort.Strings(ids) // deterministic URL for caching layers + test reproducibility
|
||||
|
||||
q := url.Values{}
|
||||
q.Set("ids", strings.Join(ids, ","))
|
||||
q.Set("vs_currencies", "usd")
|
||||
endpoint := strings.TrimRight(f.BaseURL, "/") + "/simple/price?" + q.Encode()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("coingecko: build request: %w", err)
|
||||
}
|
||||
req.Header.Set("Accept", "application/json")
|
||||
if f.APIKey != "" {
|
||||
req.Header.Set("x-cg-pro-api-key", f.APIKey)
|
||||
}
|
||||
|
||||
client := f.HTTPClient
|
||||
if client == nil {
|
||||
client = &http.Client{Timeout: DefaultCoinGeckoTimeout}
|
||||
}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("coingecko: http: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
body, _ := io.ReadAll(io.LimitReader(resp.Body, 512))
|
||||
return fmt.Errorf("coingecko: status %d: %s", resp.StatusCode, strings.TrimSpace(string(body)))
|
||||
}
|
||||
|
||||
var payload map[string]map[string]float64
|
||||
if err := json.NewDecoder(resp.Body).Decode(&payload); err != nil {
|
||||
return fmt.Errorf("coingecko: decode: %w", err)
|
||||
}
|
||||
|
||||
next := make(map[string]float64, len(f.IDMap))
|
||||
for sym, id := range f.IDMap {
|
||||
if row, ok := payload[id]; ok {
|
||||
if usd, ok := row["usd"]; ok && usd > 0 {
|
||||
next[sym] = usd
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
f.mu.Lock()
|
||||
f.cache = next
|
||||
f.cachedAt = time.Now()
|
||||
f.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// FallbackFeed
|
||||
// =============================================================================
|
||||
|
||||
// FallbackFeed tries Primary first and falls back to Secondary on any
|
||||
// error. Use it to wrap a CoinGeckoFeed (which doesn't know about LUX /
|
||||
// ZOO) over a StaticPriceFeed seeded with the missing assets — and to
|
||||
// keep quoting live during CoinGecko outages.
|
||||
type FallbackFeed struct {
|
||||
Primary PriceFeed
|
||||
Secondary PriceFeed
|
||||
// OnFallback is called when Primary errors and Secondary is consulted.
|
||||
// Optional; used for ops logging. The error from Primary is passed in.
|
||||
OnFallback func(asset string, err error)
|
||||
}
|
||||
|
||||
// Price returns Primary's price, or Secondary's if Primary errored.
|
||||
func (f *FallbackFeed) Price(ctx context.Context, asset string) (float64, error) {
|
||||
if f.Primary != nil {
|
||||
v, err := f.Primary.Price(ctx, asset)
|
||||
if err == nil {
|
||||
return v, nil
|
||||
}
|
||||
if f.OnFallback != nil {
|
||||
f.OnFallback(asset, err)
|
||||
}
|
||||
if f.Secondary == nil {
|
||||
return 0, err
|
||||
}
|
||||
v2, err2 := f.Secondary.Price(ctx, asset)
|
||||
if err2 == nil {
|
||||
return v2, nil
|
||||
}
|
||||
// Both failed. If Primary said ErrPriceUnknown, the Secondary
|
||||
// error is what callers want (it explains why the fallback
|
||||
// also doesn't know). Otherwise surface the Primary error so
|
||||
// network/auth failures aren't masked by "unknown asset".
|
||||
if errors.Is(err, ErrPriceUnknown) {
|
||||
return 0, err2
|
||||
}
|
||||
return 0, err
|
||||
}
|
||||
if f.Secondary == nil {
|
||||
return 0, errors.New("fallback_feed: no feeds configured")
|
||||
}
|
||||
return f.Secondary.Price(ctx, asset)
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// fakeCG builds an httptest.Server that answers /simple/price with
|
||||
// the supplied id→usd table and counts requests.
|
||||
func fakeCG(t *testing.T, prices map[string]float64) (*httptest.Server, *int32) {
|
||||
t.Helper()
|
||||
var calls int32
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
atomic.AddInt32(&calls, 1)
|
||||
if !strings.HasSuffix(r.URL.Path, "/simple/price") {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
var b strings.Builder
|
||||
b.WriteString("{")
|
||||
first := true
|
||||
for id, usd := range prices {
|
||||
if !first {
|
||||
b.WriteString(",")
|
||||
}
|
||||
first = false
|
||||
fmt.Fprintf(&b, "%q:{\"usd\":%v}", id, usd)
|
||||
}
|
||||
b.WriteString("}")
|
||||
_, _ = w.Write([]byte(b.String()))
|
||||
}))
|
||||
t.Cleanup(srv.Close)
|
||||
return srv, &calls
|
||||
}
|
||||
|
||||
func TestCoinGeckoFeed_ReturnsMappedPrice(t *testing.T) {
|
||||
srv, calls := fakeCG(t, map[string]float64{
|
||||
"ethereum": 4200.0,
|
||||
"bitcoin": 70000.0,
|
||||
})
|
||||
|
||||
f := NewCoinGeckoFeed(srv.URL, "", map[string]string{
|
||||
"ETH": "ethereum",
|
||||
"BTC": "bitcoin",
|
||||
})
|
||||
|
||||
v, err := f.Price(context.Background(), "ETH")
|
||||
if err != nil {
|
||||
t.Fatalf("ETH: %v", err)
|
||||
}
|
||||
if v != 4200.0 {
|
||||
t.Errorf("ETH = %v, want 4200", v)
|
||||
}
|
||||
v, err = f.Price(context.Background(), "btc") // case-insensitive
|
||||
if err != nil {
|
||||
t.Fatalf("BTC: %v", err)
|
||||
}
|
||||
if v != 70000.0 {
|
||||
t.Errorf("BTC = %v, want 70000", v)
|
||||
}
|
||||
|
||||
if got := atomic.LoadInt32(calls); got != 1 {
|
||||
t.Errorf("expected 1 batched HTTP call, got %d", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCoinGeckoFeed_UnknownSymbol_ReturnsErrPriceUnknown(t *testing.T) {
|
||||
srv, _ := fakeCG(t, map[string]float64{"ethereum": 4200})
|
||||
f := NewCoinGeckoFeed(srv.URL, "", map[string]string{"ETH": "ethereum"})
|
||||
|
||||
_, err := f.Price(context.Background(), "LUX")
|
||||
if !errors.Is(err, ErrPriceUnknown) {
|
||||
t.Fatalf("LUX expected ErrPriceUnknown, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCoinGeckoFeed_CachesWithinTTL(t *testing.T) {
|
||||
srv, calls := fakeCG(t, map[string]float64{"ethereum": 4200})
|
||||
f := NewCoinGeckoFeed(srv.URL, "", map[string]string{"ETH": "ethereum"})
|
||||
f.CacheTTL = 5 * time.Second
|
||||
|
||||
for i := 0; i < 5; i++ {
|
||||
if _, err := f.Price(context.Background(), "ETH"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if got := atomic.LoadInt32(calls); got != 1 {
|
||||
t.Errorf("expected single fetch within TTL, got %d", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCoinGeckoFeed_HTTPError_Propagates(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
http.Error(w, "rate limited", http.StatusTooManyRequests)
|
||||
}))
|
||||
t.Cleanup(srv.Close)
|
||||
|
||||
f := NewCoinGeckoFeed(srv.URL, "", map[string]string{"ETH": "ethereum"})
|
||||
_, err := f.Price(context.Background(), "ETH")
|
||||
if err == nil {
|
||||
t.Fatal("expected error on 429, got nil")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "429") {
|
||||
t.Errorf("expected error to mention 429, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCoinGeckoFeed_SendsAPIKeyHeader(t *testing.T) {
|
||||
var seenKey string
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
seenKey = r.Header.Get("x-cg-pro-api-key")
|
||||
_, _ = w.Write([]byte(`{"ethereum":{"usd":1}}`))
|
||||
}))
|
||||
t.Cleanup(srv.Close)
|
||||
|
||||
f := NewCoinGeckoFeed(srv.URL, "secret-key", map[string]string{"ETH": "ethereum"})
|
||||
if _, err := f.Price(context.Background(), "ETH"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if seenKey != "secret-key" {
|
||||
t.Errorf("api key header = %q, want %q", seenKey, "secret-key")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFallbackFeed_PrimarySucceeds(t *testing.T) {
|
||||
p := NewStaticPriceFeed(map[string]float64{"ETH": 4000})
|
||||
s := NewStaticPriceFeed(map[string]float64{"ETH": 100})
|
||||
f := &FallbackFeed{Primary: p, Secondary: s}
|
||||
|
||||
v, err := f.Price(context.Background(), "ETH")
|
||||
if err != nil || v != 4000 {
|
||||
t.Errorf("ETH = %v err=%v, want 4000 nil", v, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFallbackFeed_FallsBackOnUnknown(t *testing.T) {
|
||||
p := NewStaticPriceFeed(nil) // knows nothing
|
||||
s := NewStaticPriceFeed(map[string]float64{"LUX": 2.5})
|
||||
|
||||
called := false
|
||||
f := &FallbackFeed{
|
||||
Primary: p, Secondary: s,
|
||||
OnFallback: func(asset string, err error) { called = true },
|
||||
}
|
||||
v, err := f.Price(context.Background(), "LUX")
|
||||
if err != nil || v != 2.5 {
|
||||
t.Errorf("LUX = %v err=%v, want 2.5 nil", v, err)
|
||||
}
|
||||
if !called {
|
||||
t.Error("OnFallback hook not called")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFallbackFeed_BothFail_SurfacesNetworkError(t *testing.T) {
|
||||
// Primary errors with a non-Unknown error (simulating rate-limit /
|
||||
// network failure). Secondary doesn't know the asset either. The
|
||||
// FallbackFeed should surface the network error, not the asset-
|
||||
// unknown one — operators need to see CoinGecko is broken.
|
||||
primary := errFeed{err: errors.New("network down")}
|
||||
secondary := NewStaticPriceFeed(nil)
|
||||
f := &FallbackFeed{Primary: primary, Secondary: secondary}
|
||||
|
||||
_, err := f.Price(context.Background(), "ETH")
|
||||
if err == nil || !strings.Contains(err.Error(), "network down") {
|
||||
t.Errorf("expected network-down surfaced, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
type errFeed struct{ err error }
|
||||
|
||||
func (e errFeed) Price(_ context.Context, _ string) (float64, error) { return 0, e.err }
|
||||
+53
-6
@@ -28,8 +28,10 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
@@ -134,6 +136,16 @@ func main() {
|
||||
"path to a JSON file that persists per-destination-network release wallets (the long-lived MPC addresses that pay out settlements). When set, the bridge mints a release wallet on first need and reuses it across restarts. When empty AND --data-dir is set, defaults to <data-dir>/release-wallets.json. When empty AND --data-dir is empty, runs in-memory — every restart re-mints and any liquidity at the old address is stranded. Required for prod.")
|
||||
profileFlag := flag.String("profile", envOr("BRIDGE_PROFILE", "classical-compat"),
|
||||
"bridge security profile: strict-pq | classical-compat")
|
||||
coingeckoEnabled := flag.Bool("coingecko", envBool("BRIDGE_COINGECKO", false),
|
||||
"enable CoinGecko HTTP price feed (wrapped over the static feed as a fallback for LUX/ZOO and outages). When disabled (default), the static feed is the sole source of prices.")
|
||||
coingeckoURL := flag.String("coingecko-url", envOr("BRIDGE_COINGECKO_URL", DefaultCoinGeckoBaseURL),
|
||||
"CoinGecko API base URL")
|
||||
coingeckoAPIKey := flag.String("coingecko-api-key", envOr("BRIDGE_COINGECKO_API_KEY", ""),
|
||||
"CoinGecko Pro API key (empty for the free tier)")
|
||||
coingeckoTTL := flag.Duration("coingecko-cache-ttl", DefaultCoinGeckoCacheTTL,
|
||||
"how long CoinGecko prices are cached before re-fetching. The fetch batches every configured symbol into one HTTP call.")
|
||||
coingeckoTimeout := flag.Duration("coingecko-timeout", DefaultCoinGeckoTimeout,
|
||||
"per-request HTTP timeout for CoinGecko calls")
|
||||
showVersion := flag.Bool("version", false, "print version and exit")
|
||||
flag.Parse()
|
||||
|
||||
@@ -300,11 +312,12 @@ func main() {
|
||||
}
|
||||
}
|
||||
}()
|
||||
// Seed the static price feed with the values used in dev / first
|
||||
// deploys. Real production wiring should swap in a CoinGecko / Pyth
|
||||
// PriceFeed implementation. These match the order-of-magnitude
|
||||
// prices the TS app/server queries via getTokenPrice.
|
||||
priceFeed := NewStaticPriceFeed(map[string]float64{
|
||||
// Static feed seeds the assets CoinGecko doesn't list (LUX, ZOO) and
|
||||
// serves as the fallback when CoinGecko is unreachable. Order-of-
|
||||
// magnitude values matching the TS app/server's getTokenPrice — the
|
||||
// CG-backed entries are overridden at runtime, the rest are the
|
||||
// permanent source for LUX/ZOO.
|
||||
staticFeed := NewStaticPriceFeed(map[string]float64{
|
||||
"ETH": 3500.00,
|
||||
"LUX": 2.50,
|
||||
"ZOO": 0.05,
|
||||
@@ -316,11 +329,31 @@ func main() {
|
||||
"DAI": 1.00,
|
||||
"BNB": 600.00,
|
||||
})
|
||||
|
||||
var priceFeed PriceFeed = staticFeed
|
||||
feedLabel := "static"
|
||||
if *coingeckoEnabled {
|
||||
cg := NewCoinGeckoFeed(*coingeckoURL, *coingeckoAPIKey, nil)
|
||||
cg.CacheTTL = *coingeckoTTL
|
||||
cg.HTTPClient = &http.Client{Timeout: *coingeckoTimeout}
|
||||
priceFeed = &FallbackFeed{
|
||||
Primary: cg,
|
||||
Secondary: staticFeed,
|
||||
OnFallback: func(asset string, err error) {
|
||||
if errors.Is(err, ErrPriceUnknown) {
|
||||
return // expected for LUX/ZOO — not worth a log line per quote
|
||||
}
|
||||
logger.Warn("coingecko price fallback", "asset", asset, "err", err)
|
||||
},
|
||||
}
|
||||
feedLabel = "coingecko+static"
|
||||
}
|
||||
|
||||
quoteEngine := &QuoteEngine{Feed: priceFeed}
|
||||
logger.Info("native swap CRUD enabled",
|
||||
"store", storeLabel,
|
||||
"data_dir", *dataDir,
|
||||
"feed", "static",
|
||||
"feed", feedLabel,
|
||||
"fee_rate", quoteEngine.FeeRate,
|
||||
)
|
||||
|
||||
@@ -585,6 +618,20 @@ func envOr(k, fallback string) string {
|
||||
return fallback
|
||||
}
|
||||
|
||||
func envBool(k string, fallback bool) bool {
|
||||
v := os.Getenv(k)
|
||||
if v == "" {
|
||||
return fallback
|
||||
}
|
||||
switch strings.ToLower(v) {
|
||||
case "1", "t", "true", "yes", "y", "on":
|
||||
return true
|
||||
case "0", "f", "false", "no", "n", "off":
|
||||
return false
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
// selectProfile resolves the --profile flag value to a bridge profile
|
||||
// pointer. Refuses an unknown value rather than silently defaulting:
|
||||
// an operator who typos the profile name must see the error rather
|
||||
|
||||
Reference in New Issue
Block a user