A production-ready oracle feeder for Terra Classic validators. Single Go binary, high performance, 20+ price sources.
Key Features: Price aggregation β’ Secure voting β’ Low resource usage (<100MB) β’ Fast startup (<1s)
git clone https://github.com/StrathCole/oracle-go.git
cd oracle-go
make buildcp config/config.yaml config/my-config.yaml
# Edit: validators, mnemonic_env, grpc_endpointsexport ORACLE_FEEDER_MNEMONIC="your 24-word mnemonic"
./build/oracle-go --config config/my-config.yamlcurl http://localhost:8080/health # Price server status
curl http://localhost:9091/metrics | grep oracle_ # Metricsmode: both # "server", "feeder", or "both"
feeder:
chain_id: columbus-5
validators:
- terravaloper1xxx... # Your validator address
mnemonic_env: ORACLE_FEEDER_MNEMONIC
grpc_endpoints:
- host: terra-classic-grpc.publicnode.com
port: 443
tls: true
rpc_endpoints:
- host: terra-classic-rpc.publicnode.com
port: 443
tls: true
sources:
- type: cex
name: binance
enabled: true
config:
pairs:
LUNC/USDT: LUNCUSDTSee config/config.yaml for complete reference with all options.
Runtime Modes (click to expand)
| Mode | Description | Use Case |
|---|---|---|
both (default) |
Price server + Feeder | Single-server deployment |
server |
Price server only | Shared price feed for multiple validators |
feeder |
Feeder only | Connect to external price server |
# Price server only
./build/oracle-go --config config.yaml --server
# Feeder only
./build/oracle-go --config config.yaml --feeder[Unit]
Description=Terra Classic Oracle
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=oracle
Group=oracle
WorkingDirectory=/opt/oracle-go
ExecStart=/opt/oracle-go/oracle-go --config /opt/oracle-go/config/config.yaml
Restart=always
RestartSec=10
Environment="ORACLE_FEEDER_MNEMONIC=<your_mnemonic_here>"
[Install]
WantedBy=multi-user.targetEnable and start:
sudo systemctl daemon-reload
sudo systemctl enable oracle-go
sudo systemctl start oracle-go
sudo journalctl -u oracle-go -fBuild the image:
docker build -t oracle-go:latest .Run the container:
docker run -d \
--name oracle-go \
-e ORACLE_FEEDER_MNEMONIC="your 24-word mnemonic" \
-p 8080:8080 \
-p 8081:8081 \
-p 9091:9091 \
-v $(pwd)/config/config.yaml:/oracle-go/config/config.yaml \
oracle-go:latestView logs:
docker logs -f oracle-goStop container:
docker stop oracle-gocurl http://localhost:8080/health # {"status":"ok"}
curl http://localhost:9091/metrics | grep oracle_Price Server:
oracle_vote_submissions_total{status="success|failure"}- Vote countoracle_price_staleness_seconds{source="binance"}- Price freshnessoracle_source_health{source="binance"}- UP/DOWN (1/0)
Feeder:
oracle_vote_errors_total- Voting errorsoracle_lcd_failovers_total- LCD endpoint failovers
- alert: OracleVoteFailure
expr: rate(oracle_vote_submissions_total{status="failure"}[5m]) > 0.1
annotations:
summary: "Vote failure rate > 10%"
- alert: SourceDown
expr: oracle_source_health == 0
annotations:
summary: "Source {{ $labels.source }} is down"
- alert: PriceStale
expr: oracle_price_staleness_seconds > 300
annotations:
summary: "Price {{ $labels.symbol }} stale (>5min)"Centralized Exchanges (CEX): Binance, CoinGecko, Kraken, Kucoin, Huobi, Bitfinex, Bybit, Gate.io, OKX, MEXC, CoinMarketCap
Decentralized (DEX): Terraswap, Terraport, Garuda, PancakeSwap
Oracle Aggregators: Band Protocol
Fiat: ExchangeRate-API, Fixer, Frankfurter, IMF
Source Details (click to expand)
| Exchange | WebSocket | Notes |
|---|---|---|
| Binance | β | Primary LUNC source |
| CoinGecko | β | Free: 10-30 calls/min |
| Kraken | β | Good for BTC/ETH |
| Kucoin | β | LUNC trading pairs |
| Huobi | β | Asia-focused |
| Bitfinex | β | BTC/ETH only |
| Bybit | β | Derivatives focus |
| Gate.io | β | Wide altcoin range |
| OKX | β | No LUNC pairs |
| MEXC | β | Emerging altcoins |
| CoinMarketCap | β | API key required |
| DEX | Symbol |
|---|---|
| Terraswap | LUNC/USDC |
| Terraport | LUNC/USDC |
| Garuda | LUNC/USDC |
| DEX | Symbol |
|---|---|
| PancakeSwap | LUNC/USDT |
| Source | Notes |
|---|---|
| ExchangeRate-API | Free tier: 1500 requests/month |
| Fixer | API key required |
| Frankfurter | Free, no key |
| IMF | Free, no key, web-scraper |
Automatically calculated from IMF rates (USD, EUR, CNY, JPY, GBP).
| Aggregator | Notes |
|---|---|
| Band Protocol | Decentralized oracle |
# Check endpoint
curl https://terra-classic-lcd.publicnode.com/cosmos/base/tendermint/v1beta1/node_info
# Add multiple fallback endpoints- Verify 12 or 24 words
- Check
coin_type: 330(Terra Classic) - Ensure env var set:
echo $ORACLE_FEEDER_MNEMONIC
terrad query oracle params -o json | jq -r '.params.whitelist[].name'
# Verify sources provide denoms in whitelist- Check source-specific logs:
journalctl -u oracle-go | grep source - Verify API keys if required
- Test endpoint manually
logging:
level: debugjournalctl -u oracle-go -f # View all logs
journalctl -u oracle-go | grep "vote" # Vote-only logs
journalctl -u oracle-go | grep "source" # Source errorsfeeder:
dry_run: true
verify: true # Compare with on-chain ratesor
./build/oracle-go --feeder --dry-run --verifyThis will:
- β Connect to RPC/gRPC
- β Fetch prices
- β Generate vote messages
- β Verify against on-chain rates
- β NOT submit transactions
git clone https://github.com/StrathCole/oracle-go.git
cd oracle-go
go mod tidy
make build
make test # Run tests
./build/oracle-go - Create source file:
pkg/server/sources/{type}/{name}.go
package cex
import (
"context"
"github.com/StrathCole/oracle-go/pkg/server/sources"
)
type MySource struct {
*sources.BaseSource
// Add any custom fields (API client, etc.)
}
// NewMySource creates a new source instance
func NewMySource(config map[string]interface{}) (sources.Source, error) {
logger := sources.GetLoggerFromConfig(config)
// Parse pair mappings from config
pairs, err := sources.ParsePairsFromMap(config)
if err != nil {
return nil, fmt.Errorf("failed to parse pairs: %w", err)
}
// Create base source with required methods
base := sources.NewBaseSource("mysource", sources.SourceTypeCEX, pairs, logger)
return &MySource{
BaseSource: base,
// Initialize custom fields
}, nil
}
func (s *MySource) Initialize(ctx context.Context) error {
s.Logger().Info("Initializing MySource")
// Setup connections, validate config, etc.
return nil
}
func (s *MySource) Start(ctx context.Context) error {
s.Logger().Info("Starting MySource")
// Initial fetch
if err := s.fetchPrices(ctx); err != nil {
s.Logger().Warn("Failed to fetch initial prices", "error", err)
} else {
s.SetHealthy(true) // β Important: set health after successful fetch
}
// Start polling loop
go s.updateLoop(ctx)
return nil
}
func (s *MySource) Stop() error {
s.Logger().Info("Stopping MySource")
return nil
}
func (s *MySource) fetchPrices(ctx context.Context) error {
// Fetch prices from API and call:
// s.SetPrice(symbol, price, time.Now())
return nil
}
func (s *MySource) updateLoop(ctx context.Context) {
ticker := time.NewTicker(15 * time.Second)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-s.StopChan():
return
case <-ticker.C:
if err := s.fetchPrices(ctx); err != nil {
s.Logger().Error("Failed to fetch prices", "error", err)
s.SetHealthy(false)
} else {
s.SetHealthy(true)
}
}
}
}- Register source: Add to
pkg/server/sources/{type}/register.go
func init() {
sources.Register("cex.mysource", NewMySource) // Format: "{type}.{name}"
}- Add to config:
config/config.yaml
sources:
- type: cex
name: mysource
enabled: true
weight: 1.0
config:
pairs:
"LUNC/USD": "lunc-usd"
"BTC/USD": "btc"- Required methods from BaseSource:
GetPrices(ctx)- Returns current pricesSubscribe(ch)- For subscribers to receive updatesName()- Source nameType()- Source typeSymbols()- List of symbolsIsHealthy()- Health statusLastUpdate()- Timestamp of last updateSetPrice(symbol, price, time)- Update a priceSetHealthy(bool)- Update health statusGetAllPrices()- Get all pricesStopChan()- Get stop signal channel
# Run tests
make test
make test-integration # including network calls, might fail
# Linting
make lint # golangci-lint (25+ linters)
make fmt # gofumpt formatting
# Pre-commit
make fmt && make lint && make test- Fork the repository
- Create feature branch:
git checkout -b feature/amazing-feature - Commit:
git commit -m 'Add feature' - Push:
git push origin feature/amazing-feature - Open Pull Request
This project is licensed under the MIT License - see the LICENSE file for details.