Skip to content

Commit

Permalink
Merge pull request #760 from tellor-io/improve_sources
Browse files Browse the repository at this point in the history
USDM, wUSDM, sDAI feeds and new CurveFi price source
  • Loading branch information
0xSpuddy authored Mar 13, 2024
2 parents 4da563c + 2a3dc9a commit ab5d436
Show file tree
Hide file tree
Showing 14 changed files with 335 additions and 0 deletions.
6 changes: 6 additions & 0 deletions src/telliot_feeds/feeds/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@
from telliot_feeds.feeds.reth_btc_feed import reth_btc_median_feed
from telliot_feeds.feeds.reth_usd_feed import reth_usd_median_feed
from telliot_feeds.feeds.ric_usd_feed import ric_usd_median_feed
from telliot_feeds.feeds.sdai_usd_feed import sdai_usd_median_feed
from telliot_feeds.feeds.shib_usd_feed import shib_usd_median_feed
from telliot_feeds.feeds.snapshot_feed import snapshot_feed_example
from telliot_feeds.feeds.snapshot_feed import snapshot_manual_feed
Expand All @@ -95,6 +96,7 @@
from telliot_feeds.feeds.twap_manual_feed import twap_manual_feed
from telliot_feeds.feeds.uni_usd_feed import uni_usd_median_feed
from telliot_feeds.feeds.usdc_usd_feed import usdc_usd_median_feed
from telliot_feeds.feeds.usdm_usd_feed import usdm_usd_median_feed
from telliot_feeds.feeds.usdt_usd_feed import usdt_usd_median_feed
from telliot_feeds.feeds.usdy_usd_feed import usdy_usd_median_feed
from telliot_feeds.feeds.uspce_feed import uspce_feed
Expand All @@ -105,6 +107,7 @@
from telliot_feeds.feeds.wmnt_usd_feed import wmnt_usd_median_feed
from telliot_feeds.feeds.wsteth_feed import wsteth_eth_median_feed
from telliot_feeds.feeds.wsteth_feed import wsteth_usd_median_feed
from telliot_feeds.feeds.wusdm_usd_feed import wusdm_usd_feed
from telliot_feeds.feeds.xdai_usd_feed import xdai_usd_median_feed
from telliot_feeds.feeds.yfi_usd_feed import yfi_usd_median_feed

Expand Down Expand Up @@ -196,6 +199,9 @@
"evm-bal-example": evm_balance_feed_example,
"evm-bal-current-example": evm_balance_current_feed_example,
"primeeth-eth-spot": primeeth_eth_median_feed,
"usdm-usd-spot": usdm_usd_median_feed,
"wusdm-usd-spot": wusdm_usd_feed,
"sdai-usd-spot": sdai_usd_median_feed,
}

DATAFEED_BUILDER_MAPPING: Dict[str, DataFeed[Any]] = {
Expand Down
20 changes: 20 additions & 0 deletions src/telliot_feeds/feeds/sdai_usd_feed.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from telliot_feeds.datafeed import DataFeed
from telliot_feeds.queries.price.spot_price import SpotPrice
from telliot_feeds.sources.price.spot.coingecko import CoinGeckoSpotPriceSource
from telliot_feeds.sources.price.spot.coinpaprika import CoinpaprikaSpotPriceSource
from telliot_feeds.sources.price.spot.curvefiprice import CurveFiUSDPriceSource
from telliot_feeds.sources.price_aggregator import PriceAggregator

sdai_usd_median_feed = DataFeed(
query=SpotPrice(asset="SDAI", currency="USD"),
source=PriceAggregator(
asset="sdai",
currency="usd",
algorithm="median",
sources=[
CoinGeckoSpotPriceSource(asset="sdai", currency="usd"),
CoinpaprikaSpotPriceSource(asset="sdai-savings-dai", currency="usd"),
CurveFiUSDPriceSource(asset="sdai", currency="usd"),
],
),
)
2 changes: 2 additions & 0 deletions src/telliot_feeds/feeds/trb_usd_feed.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from telliot_feeds.queries.price.spot_price import SpotPrice
from telliot_feeds.sources.price.spot.coinbase import CoinbaseSpotPriceSource
from telliot_feeds.sources.price.spot.coingecko import CoinGeckoSpotPriceSource
from telliot_feeds.sources.price.spot.coinpaprika import CoinpaprikaSpotPriceSource
from telliot_feeds.sources.price_aggregator import PriceAggregator

trb_usd_median_feed = DataFeed(
Expand All @@ -14,6 +15,7 @@
sources=[
CoinGeckoSpotPriceSource(asset="trb", currency="usd"),
CoinbaseSpotPriceSource(asset="trb", currency="usd"),
CoinpaprikaSpotPriceSource(asset="trb-tellor", currency="usd"),
],
),
)
23 changes: 23 additions & 0 deletions src/telliot_feeds/feeds/usdm_usd_feed.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from telliot_feeds.datafeed import DataFeed
from telliot_feeds.queries.price.spot_price import SpotPrice
from telliot_feeds.sources.price.spot.coingecko import CoinGeckoSpotPriceSource
from telliot_feeds.sources.price.spot.coinpaprika import CoinpaprikaSpotPriceSource
from telliot_feeds.sources.price.spot.curvefiprice import CurveFiUSDPriceSource
from telliot_feeds.sources.price.spot.uniswapV3Pool import UniswapV3PoolPriceSource
from telliot_feeds.sources.price_aggregator import PriceAggregator


usdm_usd_median_feed = DataFeed(
query=SpotPrice(asset="USDM", currency="USD"),
source=PriceAggregator(
asset="usdm",
currency="usd",
algorithm="median",
sources=[
CoinGeckoSpotPriceSource(asset="usdm", currency="usd"),
CurveFiUSDPriceSource(asset="usdm", currency="usd"),
CoinpaprikaSpotPriceSource(asset="usdm-mountain-protocol-usd", currency="usd"),
UniswapV3PoolPriceSource(asset="usdm", currency="usd"),
],
),
)
8 changes: 8 additions & 0 deletions src/telliot_feeds/feeds/wusdm_usd_feed.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from telliot_feeds.datafeed import DataFeed
from telliot_feeds.queries.price.spot_price import SpotPrice
from telliot_feeds.sources.wusdm_source import wUSDMSpotPriceSource


wusdm_usd_feed = DataFeed(
query=SpotPrice(asset="WUSDM", currency="USD"), source=wUSDMSpotPriceSource(asset="wusdm", currency="usd")
)
3 changes: 3 additions & 0 deletions src/telliot_feeds/queries/price/spot_price.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,9 @@
"USDY/USD",
"WMNT/USD",
"PRIMEETH/ETH",
"WUSDM/USD",
"SDAI/USD",
"USDM/USD",
]


Expand Down
18 changes: 18 additions & 0 deletions src/telliot_feeds/queries/query_catalog.py
Original file line number Diff line number Diff line change
Expand Up @@ -547,3 +547,21 @@
title="PRIMEETH/ETH spot price",
q=SpotPrice(asset="primeeth", currency="eth"),
)

query_catalog.add_entry(
tag="usdm-usd-spot",
title="USDM/USD spot price",
q=SpotPrice(asset="usdm", currency="usd"),
)

query_catalog.add_entry(
tag="wusdm-usd-spot",
title="WUSDM/USD spot price",
q=SpotPrice(asset="wusdm", currency="usd"),
)

query_catalog.add_entry(
tag="sdai-usd-spot",
title="SDAI/USD spot price",
q=SpotPrice(asset="sdai", currency="usd"),
)
2 changes: 2 additions & 0 deletions src/telliot_feeds/sources/price/spot/coingecko.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@
"usdy": "ondo-us-dollar-yield",
"wmnt": "wrapped-mantle",
"primeeth": "prime-staked-eth",
"usdm": "usdm",
"sdai": "savings-dai",
}


Expand Down
91 changes: 91 additions & 0 deletions src/telliot_feeds/sources/price/spot/curvefiprice.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
from dataclasses import dataclass
from dataclasses import field
from typing import Any

from telliot_feeds.dtypes.datapoint import datetime_now_utc
from telliot_feeds.dtypes.datapoint import OptionalDataPoint
from telliot_feeds.pricing.price_service import WebPriceService
from telliot_feeds.pricing.price_source import PriceSource
from telliot_feeds.utils.log import get_logger


logger = get_logger(__name__)


ethereum_contract_map = {
"usdm": "0x59D9356E565Ab3A36dD77763Fc0d87fEaf85508C",
"sdai": "0x83F20F44975D03b1b09e64809B757c47f942BEeA",
"eth": "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",
"steth": "0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84",
"btc": "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599",
}


class CurveFiUSDPriceService(WebPriceService):
"""CurveFinance Curve-Prices Service"""

def __init__(self, **kwargs: Any) -> None:
kwargs["name"] = "Curve-Prices Service"
kwargs["url"] = "https://prices.curve.fi/v1/usd_price"
super().__init__(**kwargs)

async def get_price(self, asset: str, currency: str) -> OptionalDataPoint[float]:
"""This implementation gets the price from the Curve finance API."""
asset = asset.lower()
currency = currency.lower()
if asset not in ethereum_contract_map:
logger.error(f"Asset not mapped for curve price Source: {asset}")
return None, None
asset_address = ethereum_contract_map[asset]

if currency != "usd":
logger.error("Service for usd pairs only")
return None, None

request_url = f"/ethereum/{asset_address}"

d = self.get_url(request_url)

if "error" in d:
logger.error(d)
return None, None

response = d.get("response")
if response is None:
logger.error("Error parsing Curve Finance API response, 'response' key not found")
return None, None

data = response.get("data")
if data is None:
logger.error("Error parsing Curve Finance API response 'data' key not found")
return None, None

asset_price = data.get("usd_price")
if asset_price is None:
logger.error("Failed to parse response data from Curve Finance API 'usd_price' key not found")
return None, None

isAddress = data.get("address", "").lower() == asset_address.lower()

if not isAddress:
logger.error("Asset '0x' address in API response doesn't match contract address stored")
return None, None
return asset_price, datetime_now_utc()


@dataclass
class CurveFiUSDPriceSource(PriceSource):
asset: str = ""
currency: str = ""
service: CurveFiUSDPriceService = field(default_factory=CurveFiUSDPriceService, init=False)


if __name__ == "__main__":
import asyncio

async def main() -> None:
source = CurveFiUSDPriceSource(asset="usdm", currency="usd")
v, _ = await source.fetch_new_datapoint()
print(v)

asyncio.run(main())
1 change: 1 addition & 0 deletions src/telliot_feeds/sources/price/spot/uniswapV3Pool.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
uniswapV3token0__pool_map = {
"oeth": "0x52299416c469843f4e0d54688099966a6c7d720f",
"primeeth": "0xb6934f4cf655c93e897514dc7c2af5a143b9ca22",
"usdm": "0x4a25dbdf9629b1782c3e2c7de3bdce41f1c7f801",
}


Expand Down
97 changes: 97 additions & 0 deletions src/telliot_feeds/sources/wusdm_source.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
from dataclasses import dataclass
from dataclasses import field
from typing import Any
from typing import Optional

from telliot_core.apps.telliot_config import TelliotConfig

from telliot_feeds.dtypes.datapoint import OptionalDataPoint
from telliot_feeds.pricing.price_service import WebPriceService
from telliot_feeds.pricing.price_source import PriceSource
from telliot_feeds.sources.price.spot.coingecko import CoinGeckoSpotPriceSource
from telliot_feeds.sources.price.spot.coinpaprika import CoinpaprikaSpotPriceSource
from telliot_feeds.sources.price.spot.curvefiprice import CurveFiUSDPriceSource
from telliot_feeds.sources.price.spot.uniswapV3Pool import UniswapV3PoolPriceSource
from telliot_feeds.sources.price_aggregator import PriceAggregator
from telliot_feeds.utils.log import get_logger

logger = get_logger(__name__)


class wUSDMSpotPriceService(WebPriceService):
"""Custom wUSDM Price Service"""

def __init__(self, **kwargs: Any) -> None:
kwargs["name"] = "Custom wUSDM Price Service"
kwargs["url"] = ""
super().__init__(**kwargs)
self.cfg = TelliotConfig()

def get_wusdm_usd_ratio(self) -> Optional[float]:
# get endpoint
endpoint = self.cfg.endpoints.find(chain_id=1)
if not endpoint:
logger.error("Endpoint not found for mainnet to get wusdm_eth_ratio")
return None
ep = endpoint[0]
if not ep.connect():
logger.error("Unable to connect endpoint for mainnet to get wusdm_eth_ratio")
return None
w3 = ep.web3
# get ratio
wusdm_eth_ratio_bytes = w3.eth.call(
{
"to": "0x57F5E098CaD7A3D1Eed53991D4d66C45C9AF7812",
"data": "0x07a2d13a0000000000000000000000000000000000000000000000000de0b6b3a7640000",
}
)
wusdm_usd_ratio_decoded = w3.toInt(wusdm_eth_ratio_bytes)
wusdm_usd_ratio = w3.fromWei(wusdm_usd_ratio_decoded, "ether")
print(f"Ratio from wUSDM contract: {wusdm_usd_ratio}")
return float(wusdm_usd_ratio)

async def get_price(self, asset: str, currency: str) -> OptionalDataPoint[float]:
"""This implementation gets the price of USDM from multiple sources and
calculates the price of wUSDM using the ratio of wUSDM to USDMfrom wUSDM contract.
"""
asset = asset.lower()
currency = currency.lower()

wusdm_ratio = self.get_wusdm_usd_ratio()
if wusdm_ratio is None:
logger.error("Unable to get wUSDM_USDM_ratio")
return None, None

source = PriceAggregator(
algorithm="median",
sources=[
CoinGeckoSpotPriceSource(asset="usdm", currency="usd"),
CurveFiUSDPriceSource(asset="usdm", currency="usd"),
CoinpaprikaSpotPriceSource(asset="usdm-mountain-protocol-usd", currency=currency),
UniswapV3PoolPriceSource(asset="usdm", currency=currency),
],
)

wusdm_price, timestamp = await source.fetch_new_datapoint()
if wusdm_price is None:
logger.error("Unable to get wusdm price")
return None, None
return wusdm_price * wusdm_ratio, timestamp


@dataclass
class wUSDMSpotPriceSource(PriceSource):
asset: str = ""
currency: str = ""
service: wUSDMSpotPriceService = field(default_factory=wUSDMSpotPriceService, init=False)


if __name__ == "__main__":
import asyncio

async def main() -> None:
source = wUSDMSpotPriceSource(asset="wusdm", currency="usd")
v, _ = await source.fetch_new_datapoint()
print(v)

asyncio.run(main())
22 changes: 22 additions & 0 deletions tests/feeds/test_sdai_usd_feed.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import statistics

import pytest

from telliot_feeds.feeds.sdai_usd_feed import sdai_usd_median_feed


@pytest.mark.asyncio
async def test_sdai_usd_median_feed(caplog):
"""Retrieve median SDAI/USD price."""
v, _ = await sdai_usd_median_feed.source.fetch_new_datapoint()

assert v is not None
assert v > 0
assert "sources used in aggregate: 3" in caplog.text.lower()
print(f"sDAI/USD Price: {v}")

# Get list of data sources from sources dict
source_prices = [source.latest[0] for source in sdai_usd_median_feed.source.sources if source.latest[0]]

# Make sure error is less than decimal tolerance
assert (v - statistics.median(source_prices)) < 10**-6
21 changes: 21 additions & 0 deletions tests/feeds/test_usdm_usd_feed.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import statistics

import pytest

from telliot_feeds.feeds.usdm_usd_feed import usdm_usd_median_feed


@pytest.mark.asyncio
async def test_usdm_usd_median_feed(caplog):
"""Retrieve median USDM/USD price."""
v, _ = await usdm_usd_median_feed.source.fetch_new_datapoint()

assert v is not None
assert v > 0
assert "sources used in aggregate: 4" in caplog.text.lower()
print(f"USDM/ETH Price: {v}")
# Get list of data sources from sources dict
source_prices = usdm_usd_median_feed.source.latest[0]

# Make sure error is less than decimal tolerance
assert (v - statistics.median([source_prices])) < 10**-6
Loading

0 comments on commit ab5d436

Please sign in to comment.