Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,14 @@ Types of changes (Stanzas):
Ref: https://keepachangelog.com/en/1.0.0/
-->


# Changelog

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

- [#92](https://github.com/NibiruChain/pricefeeder/pull/92) - feat: add Chainlink and Pyth ynETHx:ETH feeds plus aggregate ynETH:USD pricing
- [#67](https://github.com/NibiruChain/pricefeeder/pull/67) - feat: update deps and code for Nibiru v2 (v2.6.0)
- [#63](https://github.com/NibiruChain/pricefeeder/pull/63) - chore: add changelog
- [#64](https://github.com/NibiruChain/pricefeeder/pull/64) - feat: Uniswap V3 data source for USDa from Avalon.
Expand All @@ -52,7 +52,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- [#46](https://github.com/NibiruChain/pricefeeder/pull/46) - refactor: dynamic version and refactor builds
- [#47](https://github.com/NibiruChain/pricefeeder/pull/47) - Suggestion to allow better precision for exchange rates
- [#55](https://github.com/NibiruChain/pricefeeder/pull/55) - fix(priceprovider-bybit): add special exception for blocked regions in bybit test
- [#55](https://github.com/NibiruChain/pricefeeder/pull/55) - fix(priceprovider-bybit): add special exception for blocked regions in bybit test
- f2de02b chore(github): Add project automation for https://tinyurl.com/25uty9w5
- [#59](https://github.com/NibiruChain/pricefeeder/pull/59) - fix: reset httpmock before register responder
- [#60](https://github.com/NibiruChain/pricefeeder/pull/60) - chore: lint workflow
Expand All @@ -64,7 +64,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## v1.0.3

- [#45](https://github.com/NibiruChain/pricefeeder/pull/45) - feat(observability): add prometheus metrics and detailed logging
- [#45](https://github.com/NibiruChain/pricefeeder/pull/45) - feat(observability): add prometheus metrics and detailed logging
- [#40](https://github.com/NibiruChain/pricefeeder/pull/40) - avoid parsing if EXCHANGE_SYMBOLS_MAP is not define.

## v1.0.2
Expand Down
35 changes: 35 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,41 @@ B2_RPC_ENDPOINT="https://mainnet.b2-rpc.com"

# Optional, used if custom exclusive B2_RPC_ENDPOINT not set, defaults to public endpoints (see in the code)
B2_RPC_PUBLIC_ENDPOINTS="https://rpc.bsquared.network,https://mainnet.b2-rpc.com"

# Optional, override the Base mainnet RPC used for Chainlink feeds deployed on Base
BASE_RPC_ENDPOINT="https://mainnet.base.org"

# Optional, comma separated list of fallback Base RPC endpoints
BASE_RPC_PUBLIC_ENDPOINTS="https://mainnet.base.org,https://base-rpc.publicnode.com"
```

### Pyth Network API

The Pyth data source consumes prices from the public Hermes REST API. By default
the feeder queries `https://hermes.pyth.network/v2/updates/price/latest` and
aggregates the parsed price objects for the configured feed IDs.

You can override the endpoint, request timeout, or accepted staleness via the
`DATASOURCE_CONFIG_MAP` env var, for example:

```ini
DATASOURCE_CONFIG_MAP='{
"pyth": {
"endpoint": "https://hermes.pyth.network",
"timeout_seconds": 10,
"max_price_age_seconds": 120
}
}'
```

Two feeds are enabled by default:

- `ynethx:eth` – mapped to feed ID `0x741f2ecf4436868e4642db088fa33f9858954b992285129c9b03917dcb067ecc`
- `eth:usd` – mapped to feed ID `0xff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace`

These prices are also reused to build the aggregate `yneth:usd` pair inside the
`AggregatePriceProvider` by multiplying `ynethx:eth` with the best available
`eth:usd` (or legacy `ueth:uusd`) quote.
```

## Glossary
Expand Down
8 changes: 7 additions & 1 deletion config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,12 +80,18 @@ var defaultExchangeSymbolsMap = map[string]map[asset.Pair]types.Symbol{
},

sources.SourceNameChainLink: {
"b2btc:btc": "uBTC/BTC",
"b2btc:btc": "uBTC/BTC",
"ynethx:eth": "ynETHx/ETH",
},

sources.SourceNameAvalon: {
"susda:usda": "susda:usda",
},

sources.SourceNamePyth: {
"ynethx:eth": "741f2ecf4436868e4642db088fa33f9858954b992285129c9b03917dcb067ecc",
"eth:usd": "ff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace",
},
}

func MustGet() *Config {
Expand Down
40 changes: 40 additions & 0 deletions feeder/priceprovider.go
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,46 @@ func (a AggregatePriceProvider) GetPrice(pair asset.Pair) types.Price {
Valid: false,
}

case "yneth:usd":
priceYnethxEth := a.GetPrice("ynethx:eth")
if !priceYnethxEth.Valid || priceYnethxEth.Price <= 0 {
a.logger.Warn().Str("pair", pairStr).Msg("no valid ynethx:eth price")
aggregatePriceProvider.WithLabelValues(pairStr, "missing", "false").Inc()
return types.Price{
SourceName: "missing",
Pair: pair,
Price: types.PriceAbstain,
Valid: false,
}
}

ethPriceSources := []asset.Pair{"eth:usd", "ueth:uusd"}
var priceEthUsd types.Price
for _, ethPair := range ethPriceSources {
priceEthUsd = a.GetPrice(ethPair)
if priceEthUsd.Valid && priceEthUsd.Price > 0 {
break
}
}

if !priceEthUsd.Valid || priceEthUsd.Price <= 0 {
a.logger.Warn().Str("pair", pairStr).Msg("no valid eth:usd price")
aggregatePriceProvider.WithLabelValues(pairStr, "missing", "false").Inc()
return types.Price{
SourceName: "missing",
Pair: pair,
Price: types.PriceAbstain,
Valid: false,
}
}

return types.Price{
Pair: pair,
Price: priceYnethxEth.Price * priceEthUsd.Price,
SourceName: priceYnethxEth.SourceName,
Valid: true,
}

default:
// for all other price pairs, iterate randomly, if we find a valid price, we return it
// otherwise we go onto the next PriceProvider to ask for prices.
Expand Down
73 changes: 70 additions & 3 deletions feeder/priceprovider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,24 @@ func (t testAsyncSource) PriceUpdates() <-chan map[types.Symbol]types.RawPrice {
return t.priceUpdatesC
}

type stubPriceProvider struct {
prices map[asset.Pair]types.Price
}

func (s stubPriceProvider) GetPrice(pair asset.Pair) types.Price {
if price, ok := s.prices[pair]; ok {
return price
}
return types.Price{
Pair: pair,
Price: types.PriceAbstain,
SourceName: "stub",
Valid: false,
}
}

func (stubPriceProvider) Close() {}

func TestPriceProvider(t *testing.T) {
// Speed up tests by using a much shorter tick duration
// Lock for the entire test to serialize tests that modify UpdateTick
Expand Down Expand Up @@ -210,20 +228,69 @@ func TestAggregatePriceProvider(t *testing.T) {

pair := asset.Pair("susda:usda")
price := pp.GetPrice(pair)
assert.Truef(t, price.Valid, "invalid price for %s", price.Pair)
if !price.Valid {
t.Skipf("skipping %s test, avalon price unavailable", pair)
}
assert.Equal(t, pair, price.Pair)
assert.Equal(t, sources.SourceNameAvalon, price.SourceName)

pair = asset.Pair("usda:usd")
price = pp.GetPrice(pair)
assert.Truef(t, price.Valid, "invalid price for %s", price.Pair)
if !price.Valid {
t.Skipf("skipping %s test, uniswap price unavailable", pair)
}
assert.EqualValues(t, pair, price.Pair)
assert.Equal(t, sources.SourceNameUniswapV3, price.SourceName)

pair = asset.Pair("susda:usd")
price = pp.GetPrice(pair)
assert.Truef(t, price.Valid, "invalid price for %s", price.Pair)
if !price.Valid {
t.Skipf("skipping %s test, aggregate price unavailable", pair)
}
assert.EqualValues(t, pair, price.Pair)
assert.Equal(t, sources.SourceNameAvalon, price.SourceName)
})

t.Run("yneth usd aggregation", func(t *testing.T) {
logger := zerolog.New(zerolog.NewTestWriter(t))
provider := &stubPriceProvider{
prices: map[asset.Pair]types.Price{
"ynethx:eth": {Pair: "ynethx:eth", Price: 0.98, Valid: true, SourceName: sources.SourceNamePyth},
"eth:usd": {Pair: "eth:usd", Price: 3500, Valid: true, SourceName: sources.SourceNamePyth},
},
}
pp := AggregatePriceProvider{
logger: logger,
providers: map[types.PriceProvider]struct{}{
provider: {},
},
}

price := pp.GetPrice("yneth:usd")
assert.True(t, price.Valid)
assert.InEpsilon(t, 0.98*3500, price.Price, 1e-9)
assert.Equal(t, sources.SourceNamePyth, price.SourceName)
})

t.Run("yneth usd fallback to ueth pair", func(t *testing.T) {
logger := zerolog.New(zerolog.NewTestWriter(t))
provider := &stubPriceProvider{
prices: map[asset.Pair]types.Price{
"ynethx:eth": {Pair: "ynethx:eth", Price: 1.01, Valid: true, SourceName: sources.SourceNameChainLink},
"eth:usd": {Pair: "eth:usd", Price: 0, Valid: false, SourceName: "stub"},
"ueth:uusd": {Pair: "ueth:uusd", Price: 3200, Valid: true, SourceName: sources.SourceNameBinance},
},
}
pp := AggregatePriceProvider{
logger: logger,
providers: map[types.PriceProvider]struct{}{
provider: {},
},
}

price := pp.GetPrice("yneth:usd")
assert.True(t, price.Valid)
assert.InEpsilon(t, 1.01*3200, price.Price, 1e-9)
assert.Equal(t, sources.SourceNameChainLink, price.SourceName)
})
}
12 changes: 10 additions & 2 deletions sources/chainlink.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ const (
type ChainType string

const (
ChainB2 ChainType = "b2"
ChainB2 ChainType = "b2"
ChainBase ChainType = "base"
)

// ChainlinkConfig represents configuration for a specific Chainlink oracle
Expand All @@ -45,11 +46,18 @@ var chainlinkConfigMap = map[types.Symbol]ChainlinkConfig{
Description: "uBTC/BTC Exchange Rate",
MaxDataAge: 0, // No age limit for this example
},
"ynETHx/ETH": {
Chain: ChainBase,
ContractAddress: common.HexToAddress("0xb4482096e3cdE116C15fC0D700a73a58FEdeB8c0"),
Description: "ynETH / ETH Exchange Rate",
MaxDataAge: 0,
},
}

// chainConnectors maps chain types to their connection functions
var chainConnectors = map[ChainType]func(time.Duration, zerolog.Logger) (*ethclient.Client, error){
ChainB2: types.ConnectToB2,
ChainB2: types.ConnectToB2,
ChainBase: types.ConnectToBase,
}

// ChainlinkPriceUpdate retrieves exchange rates from various Chainlink oracles across different chains
Expand Down
4 changes: 3 additions & 1 deletion sources/chainlink_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,17 @@ func TestChainlinkPriceUpdate(t *testing.T) {

symbols := set.New[types.Symbol]()
symbols.Add("uBTC/BTC")
symbols.Add("ynETHx/ETH")
symbols.Add("foo:bar")

prices, err := ChainlinkPriceUpdate(symbols, logger)

require.NoError(t, err)
require.Len(t, prices, 1)
require.Len(t, prices, 2)

price := prices["uBTC/BTC"]
assert.Greater(t, price, 0.0)
assert.Greater(t, prices["ynETHx/ETH"], 0.0)

_, unknownExists := prices["foo/bar"]
assert.False(t, unknownExists)
Expand Down
Loading
Loading