Skip to content

Commit

Permalink
Merge pull request #799 from tellor-io/stone-usd-spot
Browse files Browse the repository at this point in the history
STONE/USD Spot With 3 Sources
  • Loading branch information
0xSpuddy authored Sep 10, 2024
2 parents ed95f80 + 68201bd commit 90fe06a
Show file tree
Hide file tree
Showing 6 changed files with 187 additions and 0 deletions.
2 changes: 2 additions & 0 deletions src/telliot_feeds/feeds/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@
from telliot_feeds.feeds.spot_price_manual_feed import spot_price_manual_feed
from telliot_feeds.feeds.steth_btc_feed import steth_btc_median_feed
from telliot_feeds.feeds.steth_usd_feed import steth_usd_median_feed
from telliot_feeds.feeds.stone_usd_feed import stone_usd_median_feed
from telliot_feeds.feeds.string_query_feed import string_query_feed
from telliot_feeds.feeds.sushi_usd_feed import sushi_usd_median_feed
from telliot_feeds.feeds.sweth_usd_feed import sweth_usd_median_feed
Expand Down Expand Up @@ -228,6 +229,7 @@
"tlos-usd-spot": tlos_usd_median_feed,
"tara-usd-spot": tara_usd_median_feed,
"pufeth-usd-spot": pufeth_usd_median_feed,
"stone-usd-spot": stone_usd_median_feed,
}

DATAFEED_BUILDER_MAPPING: Dict[str, DataFeed[Any]] = {
Expand Down
21 changes: 21 additions & 0 deletions src/telliot_feeds/feeds/stone_usd_feed.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
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.nuri import nuriSpotPriceSource
from telliot_feeds.sources.price_aggregator import PriceAggregator


stone_usd_median_feed = DataFeed(
query=SpotPrice(asset="stone", currency="usd"),
source=PriceAggregator(
asset="stone",
currency="usd",
algorithm="median",
sources=[
CoinGeckoSpotPriceSource(asset="stone", currency="usd"),
CoinpaprikaSpotPriceSource(asset="stone-stakestone-ether", currency="usd"),
nuriSpotPriceSource(asset="stone", currency="usd"),
],
),
)
1 change: 1 addition & 0 deletions src/telliot_feeds/queries/price/spot_price.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@
"ATLA/USD",
"TARA/USD",
"PUFETH/USD",
"STONE/USD",
]


Expand Down
6 changes: 6 additions & 0 deletions src/telliot_feeds/queries/query_catalog.py
Original file line number Diff line number Diff line change
Expand Up @@ -650,3 +650,9 @@
title="PUFETH/USD spot price",
q=SpotPrice(asset="pufeth", currency="usd"),
)

query_catalog.add_entry(
tag="stone-usd-spot",
title="STONE/USD spot price",
q=SpotPrice(asset="stone", currency="usd"),
)
135 changes: 135 additions & 0 deletions src/telliot_feeds/sources/price/spot/nuri.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
from dataclasses import dataclass
from dataclasses import field
from typing import Any

import requests
from requests import Session
from telliot_core.apps.telliot_config import TelliotConfig

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__)

# use the pool address here if token0
nuri_token0__pool_map = {
"stone": "0x97a90e651b0a5cf76484513469249d9bffe4c73b",
}

# use the pool address here if token1
nuri_token1__pool_map = {
"weth": "0x5300000000000000000000000000000000000004",
}

API_KEY = TelliotConfig().api_keys.find(name="thegraph")[0].key


class nuriPriceService(WebPriceService):
"""nuri Price Service for Pool Ratios"""

def __init__(self, **kwargs: Any) -> None:
kwargs["name"] = "Nuri Price Service"
kwargs["url"] = "https://gateway.thegraph.com/api"
kwargs["timeout"] = 10.0
super().__init__(**kwargs)

async def get_price(self, asset: str, currency: str) -> OptionalDataPoint[float]:
"""Implement PriceServiceInterface
This implementation gets the price from the Nuri subgraph using the pool query
"""
asset = asset.lower()
currency = currency.lower()
pool0 = nuri_token0__pool_map.get(asset, None)
pool1 = nuri_token1__pool_map.get(asset, None)

if not pool0 and not pool1:
raise Exception("Asset not supported: {}".format(asset))

if pool0:
query = "{bundles{ethPriceUSD}pool" + f'(id: "{pool0}")' + "{ token0Price } }"
key = "token0Price"

if pool1:
query = "{bundles{ethPriceUSD}pool" + f'(id: "{pool1}")' + "{ token1Price } }"
key = "token1Price"

headers = {
"Content-Type": "application/json",
}

json_data = {"query": query}

request_url = f"{self.url}/subgraphs/id/Eqr2CueSusTohoTsXCiQgQbaApjuK2ikFvpqkVTPo1y5"
logger.info(f"{request_url}")

session = Session()
if API_KEY != "":
headers = {"Accepts": "application/json", "Authorization": f"Bearer {API_KEY}"}
session.headers.update(headers)

with requests.Session() as s:
try:
r = s.post(request_url, headers=headers, json=json_data, timeout=self.timeout)
res = r.json()
data = {"response": res}
logger.info(f"{data}")

except requests.exceptions.ConnectTimeout:
logger.warning("Timeout Error, No pool prices retrieved from Nuri")
return None, None

except Exception:
logger.warning("No pool prices retrieved from Nuri")
return None, None

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

elif "response" in data:
response = data["response"]
eth_usd_price = None
token_price = None
try:
eth_usd_price = float(response["data"]["bundles"][0]["ethPriceUSD"])
logger.info(f"eth price: {eth_usd_price}")
if currency == "usd":
token_price = (float(response["data"]["pool"][key])) * eth_usd_price
elif currency == "eth":
vs_eth_usd_price = (float(response["data"]["pool"][key])) * eth_usd_price
token_price = vs_eth_usd_price / eth_usd_price
return token_price, datetime_now_utc()

if not token_price:
logger.error("Nuri was reached, but query failed! (check source)")
return None, None

except KeyError as e:
msg = "Error parsing Nuri pool response: KeyError: {}".format(e)
logger.critical(msg)
return None, None

else:
raise Exception("Invalid response from get_url")


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


if __name__ == "__main__":
import asyncio

async def main() -> None:
price_source = nuriSpotPriceSource(asset="stone", currency="usd")
price, timestamp = await price_source.fetch_new_datapoint()
print(price, timestamp)

asyncio.run(main())
22 changes: 22 additions & 0 deletions tests/feeds/test_stone_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.stone_usd_feed import stone_usd_median_feed


@pytest.mark.asyncio
async def test_stone_usd_median_feed(caplog):
"""Retrieve median stone/usd price."""
v, _ = await stone_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"stone/usd Price: {v}")

# Get list of data sources from sources dict
source_prices = [source.latest[0] for source in stone_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

0 comments on commit 90fe06a

Please sign in to comment.