astra

package
v0.1.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 7, 2026 License: MIT Imports: 21 Imported by: 0

README

Astra oracle client for Go

c, _ := astra.New(astra.Options{})
prices, _ := c.LatestPrices(ctx, []string{"0xe62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43"}, astra.PriceQuery{})
fmt.Println(prices[0].Price.Decimal()) // 83095.4425

Astra is avee's composite exchange index served over the routes and JSON shapes of Pyth's Hermes. No key is needed. This package wraps its REST, SSE and WebSocket surfaces: context-first, standard net/http, one dependency (github.com/coder/websocket).

go get github.com/aveetechapp/avee-go

Import github.com/aveetechapp/avee-go/astra; the package is astra. The default host is the preview host https://astra.preview.avee.tech; pass a base URL to use another (Options.BaseURL). A base URL with a path prefix works. Go 1.24+.

REST

Method Route
PriceFeeds(ctx, PriceFeedsQuery{Query, AssetType}) GET /v2/price_feeds
PriceFeed(ctx, id) GET /v2/price_feeds/{id}
LatestPrices(ctx, ids, PriceQuery{IgnoreInvalid}) GET /v2/updates/price/latest, every id in one request
PricesAt(ctx, publishTime, ids, PriceQuery{}) GET /v2/updates/price/{publish_time}
PricesInInterval(ctx, publishTime, seconds, ids, IntervalQuery{}) GET /v2/updates/price/{publish_time}/{interval}, flattened oldest first
Feeds(ctx, category) GET /v1/feeds: both ids, category, live value and status
FeedIDs(ctx, FeedIDsOptions{PythIDs, AstraIDs, Category}) GET /v1/feed-ids: which Pyth ids Astra serves, and their Astra ids
Status(ctx, StatusOptions{Feed}) GET /v1/status; with Feed, that feed's entry alone, and a 503 (not trading, or stale) is returned as the report, not as an error
Candles(ctx, CandlesQuery{Feed, Resolution, From, To}) GET /v1/candles as []Candle

Ids are accepted with or without 0x, in any case, are de-duplicated, and come back lower-case without 0x (NormalizeFeedID). Times are Unix seconds.

Prices are exact. Price keeps Price and Conf as integer strings with Expo, as sent:

p.Decimal()     // "83095.4425", exact
p.Float64()     // correctly rounded
p.Scaled(18)    // *big.Int 83095442500000000000000, truncated toward zero
p.ConfFloat64(); p.ConfScaled(8)

Streaming

sub, err := c.Subscribe(ctx, ids, astra.SubscribeOptions{Channel: astra.ChannelFixed1000ms})
defer sub.Close()
for u := range sub.Updates() { … }
if err := sub.Err(); err != nil { … } // set when the channel closed on a fatal error
  • Transport: TransportWebSocket (default, the Hermes WebSocket protocol) or TransportSSE (/v2/updates/price/stream, which also takes BenchmarksOnly).
  • Reconnect on any drop, 429 or 5xx: full-jitter backoff from 500 ms, capped at 30 s, reset after a connection has been up for 60 s; Retry-After is a floor. The same ids are resubscribed.
  • Keepalive: WebSocket pings every IdleTimeout/2 (45 s / 2) and reconnects when a pong is late or the subscription is not acknowledged within IdleTimeout; SSE reconnects after IdleTimeout without a byte, whatever HTTPClient.Timeout says.
  • Exactly the new values: an update older than the last one delivered for its feed, or an exact repeat of it (the replay sent on reconnect), is dropped.
  • Bounded memory: a consumer that falls behind gets the newest update per feed, never a queue; Stats().Coalesced counts what it skipped. Read buffers are reused per connection.
  • No leaks: Close (or cancelling ctx) closes the socket with 1000, waits for every goroutine, and closes Updates().
  • Fatal (no retry): 400, 404, 422, and a refused WebSocket subscription. Everything else goes to OnError and is retried. Callbacks run on the connection goroutine; keep them short, and never call Close from one (it waits for that goroutine).

Errors

Error When
*HTTPError non-2xx: Status, Body, Problem (application/problem+json), RetryAfter, Retryable()
ErrTimeout (errors.Is) a request exceeded Timeout (10 s), or a stream went idle
*ValidationError bad input, or a response that breaks the contract
*SubscriptionError the WebSocket subscribe was refused
*ServerError a non-fatal error message on an open WebSocket

Cancelling ctx returns ctx.Err() wrapped, never retried. REST calls are GETs and are retried MaxRetries times (2; DisableRetries for none) on 408, 429, 502, 503, 504, timeouts and network errors. Retry-After is honoured up to MaxRetryDelay (30 s); a longer one is returned as the error.

Find your feeds

Paste the Pyth ids your code already uses and see which ones Astra serves:

m, err := c.FeedIDs(ctx, astra.FeedIDsOptions{PythIDs: []string{"0xe62df6c8…", "0xff61491a…"}})
// m.Items:   {Symbol: "Crypto.BTC/USD", PythID: "e62d…", AstraID: "1de7…", …}
// m.Missing: Pyth ids Astra does not serve

Every id in Items works as it is on the Hermes routes: nothing in your code changes for those feeds. FeedIDs with empty options lists every feed Astra serves, and Missing is then nil. Up to MaxIDsPerURL (200) ids go in one request; a longer list is split, so the URL fits the request line the edge accepts, and merged, sorted by symbol. FeedIDsPerRequest is the same number under its old name.

Moving from Pyth Hermes

There is no official Hermes client for Go. Code calling Hermes over HTTP only changes its host to https://astra.preview.avee.tech and drops the Authorization header (it is accepted and ignored).

Hermes this module
GET /v2/updates/price/latest?ids[]=… LatestPrices(ctx, ids, PriceQuery{})
GET /v2/updates/price/stream Subscribe(ctx, ids, SubscribeOptions{Transport: TransportSSE})
wss://…/ws subscribe Subscribe(ctx, ids, SubscribeOptions{})
price * 10^expo by hand Price.Float64(), Decimal(), Scaled(n)
binary.data for updatePriceFeeds always empty: Astra is unsigned and cannot be verified on-chain

A reference-status feed is served over Hermes like a trading one; read Status or Feeds before liquidating on it.

Limits

Limit Value
Ids per call / subscription 500 (historical routes: 100 feeds)
Ids per URL MaxIDsPerURL, 200
Interval window 60 s
Candles per request 5000
Response body MaxResponseBytes, 8 MiB
Stream message MaxMessageBytes, 1 MiB; a larger WebSocket message ends the connection, which reconnects

Astra's edge rejects a request line over about 16 KB before it reaches the server, which is about 200 ids. LatestPrices sends up to 200 ids per request and splits a longer list into several, one after another, merged in the order you asked; if one of them fails, the call returns that error and no prices. An SSE subscription carries its ids in the URL, so Subscribe with TransportSSE refuses more than 200 with a *ValidationError: use the WebSocket transport (the default), where the ids travel in the subscribe message, for up to 500. | Stream lifetime | 24 h on the server; the client reconnects |

Development

go test -race ./... runs against an httptest fake Astra, including goroutine-leak checks; ASTRA_LIVE=1 go test -run Live ./... checks the preview host; go test -bench . ./... measures decoding. The contract test reads testdata/astra.yml, the synced copy of the Astra OpenAPI file, and checks every wire struct's JSON field against it.

Documentation

Index

Constants

View Source
const (
	DefaultBaseURL     = "https://astra.preview.avee.tech"
	MaxIntervalSeconds = 60
	FeedIDsPerRequest  = MaxIDsPerURL
)
View Source
const (
	MaxIDsPerRequest   = 500
	MaxIDsPerURL       = 200
	MaxHistoricalFeeds = 100
)
View Source
const (
	MinExpo          = -32
	MaxExpo          = 32
	MaxScaleDecimals = 77
)

Variables

View Source
var ErrTimeout = errors.New("astra: timeout")

Functions

func NormalizeFeedID

func NormalizeFeedID(id string) (string, error)

Types

type Candle

type Candle struct {
	Time  int64
	Open  float64
	High  float64
	Low   float64
	Close float64
}

type CandlesQuery

type CandlesQuery struct {
	Feed       string
	Resolution string
	From       int64
	To         int64
}

type Channel

type Channel string
const (
	ChannelRealTime    Channel = "real_time"
	ChannelFixed200ms  Channel = "fixed_rate@200ms"
	ChannelFixed1000ms Channel = "fixed_rate@1000ms"
)

type Client

type Client struct {
	// contains filtered or unexported fields
}

func New

func New(opts Options) (*Client, error)

func (*Client) Candles

func (c *Client) Candles(ctx context.Context, q CandlesQuery) ([]Candle, error)

func (*Client) FeedIDs

func (c *Client) FeedIDs(ctx context.Context, opts FeedIDsOptions) (FeedIDMap, error)

func (*Client) Feeds

func (c *Client) Feeds(ctx context.Context, category string) ([]Feed, error)

func (*Client) LatestPrices

func (c *Client) LatestPrices(ctx context.Context, ids []string, q PriceQuery) ([]PriceUpdate, error)

func (*Client) PriceFeed

func (c *Client) PriceFeed(ctx context.Context, id string) (FeedMetadata, error)

func (*Client) PriceFeeds

func (c *Client) PriceFeeds(ctx context.Context, q PriceFeedsQuery) ([]FeedMetadata, error)

func (*Client) PricesAt

func (c *Client) PricesAt(ctx context.Context, publishTime int64, ids []string, q PriceQuery) ([]PriceUpdate, error)

func (*Client) PricesInInterval

func (c *Client) PricesInInterval(ctx context.Context, publishTime int64, intervalSeconds int, ids []string, q IntervalQuery) ([]PriceUpdate, error)

func (*Client) Status

func (c *Client) Status(ctx context.Context, opts ...StatusOptions) (StatusReport, error)

func (*Client) Subscribe

func (c *Client) Subscribe(ctx context.Context, ids []string, opts SubscribeOptions) (*Subscription, error)

type ConnectionState

type ConnectionState string
const (
	StateConnecting   ConnectionState = "connecting"
	StateOpen         ConnectionState = "open"
	StateReconnecting ConnectionState = "reconnecting"
	StateClosed       ConnectionState = "closed"
)

type Feed

type Feed struct {
	Attributes map[string]string
	ID         string
	PythID     string
	Symbol     string
	Category   string
	Live       LiveValue
}

type FeedHealth

type FeedHealth struct {
	ID                string
	Symbol            string
	Status            FeedStatus
	AgeSeconds        float64
	PublishTime       int64
	ServedPublishTime int64
	Sources           int
	Stale             bool
}

type FeedIDEntry

type FeedIDEntry struct {
	Symbol    string
	AssetType string
	Category  string
	AstraID   string
	PythID    string
}

type FeedIDMap

type FeedIDMap struct {
	Items   []FeedIDEntry
	Missing []string
}

type FeedIDsOptions

type FeedIDsOptions struct {
	PythIDs  []string
	AstraIDs []string
	Category string
}

type FeedMetadata

type FeedMetadata struct {
	Attributes    map[string]string
	ID            string
	AstraID       string
	Symbol        string
	AssetType     string
	DisplaySymbol string
	QuoteCurrency string
	Description   string
	MinChannel    string
	Base          string
	Schedule      string
	MarketOpen    bool
}

type FeedStatus

type FeedStatus string
const (
	StatusTrading      FeedStatus = "trading"
	StatusDegraded     FeedStatus = "degraded"
	StatusMarketClosed FeedStatus = "market_closed"
	StatusReference    FeedStatus = "reference"
	StatusNoData       FeedStatus = "no_data"
)

type HTTPError

type HTTPError struct {
	Status     int
	URL        string
	Body       string
	Detail     string
	Problem    *Problem
	RetryAfter time.Duration
}

func (*HTTPError) Error

func (e *HTTPError) Error() string

func (*HTTPError) Retryable

func (e *HTTPError) Retryable() bool

type IntervalQuery

type IntervalQuery struct {
	IgnoreInvalid           bool
	KeepAllUpdatesPerSecond bool
}

type LiveValue

type LiveValue struct {
	Price             *Price
	Status            FeedStatus
	PublishTime       int64
	TimestampMs       int64
	ServedPublishTime int64
	Sources           int
	Expo              int32
}

type Options

type Options struct {
	HTTPClient       *http.Client
	Header           http.Header
	BaseURL          string
	Timeout          time.Duration
	MaxRetryDelay    time.Duration
	MaxRetries       int
	MaxResponseBytes int64
	DisableRetries   bool
}

type Price

type Price struct {
	Price       string
	Conf        string
	Expo        int32
	PublishTime int64
}

func (Price) ConfFloat64

func (p Price) ConfFloat64() float64

func (Price) ConfScaled

func (p Price) ConfScaled(decimals int) (*big.Int, error)

func (Price) Decimal

func (p Price) Decimal() string

func (Price) Float64

func (p Price) Float64() float64

func (Price) Scaled

func (p Price) Scaled(decimals int) (*big.Int, error)

type PriceFeedsQuery

type PriceFeedsQuery struct {
	Query     string
	AssetType string
}

type PriceQuery

type PriceQuery struct {
	IgnoreInvalid bool
}

type PriceUpdate

type PriceUpdate struct {
	Metadata *UpdateMetadata
	ID       string
	Price    Price
	EMAPrice Price
}

type Problem

type Problem struct {
	Type   string `json:"type"`
	Title  string `json:"title"`
	Status int    `json:"status"`
	Detail string `json:"detail"`
}

type ServerError

type ServerError struct {
	Reason string
}

func (*ServerError) Error

func (e *ServerError) Error() string

type Stats

type Stats struct {
	Connects   uint64
	Reconnects uint64
	Coalesced  uint64
	Duplicates uint64
	Invalid    uint64
}

type StatusOptions

type StatusOptions struct {
	Feed string
}

type StatusReport

type StatusReport struct {
	Feeds []FeedHealth
	Ready bool
}

type SubscribeOptions

type SubscribeOptions struct {
	OnError            func(error)
	OnStateChange      func(ConnectionState)
	Transport          Transport
	Channel            Channel
	IdleTimeout        time.Duration
	ReconnectBaseDelay time.Duration
	ReconnectMaxDelay  time.Duration
	StableAfter        time.Duration
	MaxMessageBytes    int64
	IgnoreInvalid      bool
	BenchmarksOnly     bool
}

type Subscription

type Subscription struct {
	// contains filtered or unexported fields
}

func (*Subscription) Close

func (s *Subscription) Close() error

func (*Subscription) Done

func (s *Subscription) Done() <-chan struct{}

func (*Subscription) Err

func (s *Subscription) Err() error

func (*Subscription) IDs

func (s *Subscription) IDs() []string

func (*Subscription) State

func (s *Subscription) State() ConnectionState

func (*Subscription) Stats

func (s *Subscription) Stats() Stats

func (*Subscription) Updates

func (s *Subscription) Updates() <-chan PriceUpdate

type SubscriptionError

type SubscriptionError struct {
	Reason string
}

func (*SubscriptionError) Error

func (e *SubscriptionError) Error() string

type Transport

type Transport string
const (
	TransportWebSocket Transport = "ws"
	TransportSSE       Transport = "sse"
)

type UpdateMetadata

type UpdateMetadata struct {
	ReceiveTime     int64
	PrevPublishTime int64
}

type ValidationError

type ValidationError struct {
	Reason string
}

func (*ValidationError) Error

func (e *ValidationError) Error() string

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL