diff --git a/src/telliot_feeds/feeds/__init__.py b/src/telliot_feeds/feeds/__init__.py index 3fa6751a..c6f542c4 100644 --- a/src/telliot_feeds/feeds/__init__.py +++ b/src/telliot_feeds/feeds/__init__.py @@ -93,6 +93,8 @@ 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 +from telliot_feeds.feeds.solvbtc_usd_feed import solvbtc_usd_median_feed +from telliot_feeds.feeds.solvbtcbbn_usd_feed import solvbtcbbn_usd_median_feed 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 @@ -109,6 +111,7 @@ from telliot_feeds.feeds.twap_manual_feed import twap_30d_example_manual_feed 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.unibtc_usd_feed import unibtc_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 @@ -234,6 +237,9 @@ "stone-usd-spot": stone_usd_median_feed, "superoethb-eth-spot": superoethb_eth_median_feed, "lsk-usd-spot": lsk_usd_median_feed, + "unibtc-usd-spot": unibtc_usd_median_feed, + "solvbtc-usd-spot": solvbtc_usd_median_feed, + "solvbtcbbn-usd-spot": solvbtcbbn_usd_median_feed, } DATAFEED_BUILDER_MAPPING: Dict[str, DataFeed[Any]] = { diff --git a/src/telliot_feeds/feeds/op_usd_feed.py b/src/telliot_feeds/feeds/op_usd_feed.py index 76dc76e5..81e6d162 100644 --- a/src/telliot_feeds/feeds/op_usd_feed.py +++ b/src/telliot_feeds/feeds/op_usd_feed.py @@ -3,6 +3,7 @@ from telliot_feeds.sources.price.spot.coingecko import CoinGeckoSpotPriceSource from telliot_feeds.sources.price.spot.coinpaprika import CoinpaprikaSpotPriceSource from telliot_feeds.sources.price.spot.okx import OKXSpotPriceSource +from telliot_feeds.sources.price.spot.uniV3Optimism import UniV3OptimismPriceSource from telliot_feeds.sources.price_aggregator import PriceAggregator op_usd_median_feed = DataFeed( @@ -15,6 +16,7 @@ CoinGeckoSpotPriceSource(asset="op", currency="usd"), CoinpaprikaSpotPriceSource(asset="op-optimism", currency="usd"), OKXSpotPriceSource(asset="op", currency="usdt"), + UniV3OptimismPriceSource(asset="op", currency="usdt"), ], ), ) diff --git a/src/telliot_feeds/feeds/solvbtc_usd_feed.py b/src/telliot_feeds/feeds/solvbtc_usd_feed.py new file mode 100644 index 00000000..197bd2d6 --- /dev/null +++ b/src/telliot_feeds/feeds/solvbtc_usd_feed.py @@ -0,0 +1,22 @@ +from telliot_feeds.datafeed import DataFeed +from telliot_feeds.queries.price.spot_price import SpotPrice +from telliot_feeds.sources.lfj_source import LFJPriceSource +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 + +solvbtc_usd_median_feed = DataFeed( + query=SpotPrice(asset="SOLVBTC", currency="USD"), + source=PriceAggregator( + asset="solvbtc", + currency="usd", + algorithm="median", + sources=[ + CoinGeckoSpotPriceSource(asset="solvbtc", currency="usd"), + CoinpaprikaSpotPriceSource(asset="solvbtc-solv-protocol-solvbtc", currency="usd"), + CurveFiUSDPriceSource(asset="solvbtc", currency="usd"), + LFJPriceSource(asset="solvbtc", currency="usd"), + ], + ), +) diff --git a/src/telliot_feeds/feeds/solvbtcbbn_usd_feed.py b/src/telliot_feeds/feeds/solvbtcbbn_usd_feed.py new file mode 100644 index 00000000..ae3a4b28 --- /dev/null +++ b/src/telliot_feeds/feeds/solvbtcbbn_usd_feed.py @@ -0,0 +1,20 @@ +from telliot_feeds.datafeed import DataFeed +from telliot_feeds.queries.price.spot_price import SpotPrice +from telliot_feeds.sources.lfj_source import LFJPriceSource +from telliot_feeds.sources.price.spot.coingecko import CoinGeckoSpotPriceSource +from telliot_feeds.sources.price_aggregator import PriceAggregator +from telliot_feeds.sources.solvbtcbbn_source import pancakePoolPriceSource + +solvbtcbbn_usd_median_feed = DataFeed( + query=SpotPrice(asset="SOLVBTCBBN", currency="USD"), + source=PriceAggregator( + asset="solvbtcbbn", + currency="usd", + algorithm="median", + sources=[ + CoinGeckoSpotPriceSource(asset="solvbtcbbn", currency="usd"), + pancakePoolPriceSource(asset="solvbtcbbn", currency="usd"), + LFJPriceSource(asset="solvbtcbbn", currency="usd"), + ], + ), +) diff --git a/src/telliot_feeds/feeds/unibtc_usd_feed.py b/src/telliot_feeds/feeds/unibtc_usd_feed.py new file mode 100644 index 00000000..ac9c5797 --- /dev/null +++ b/src/telliot_feeds/feeds/unibtc_usd_feed.py @@ -0,0 +1,18 @@ +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_aggregator import PriceAggregator + +unibtc_usd_median_feed = DataFeed( + query=SpotPrice(asset="UNIBTC", currency="USD"), + source=PriceAggregator( + asset="unibtc", + currency="usd", + algorithm="median", + sources=[ + CoinGeckoSpotPriceSource(asset="unibtc", currency="usd"), + CoinpaprikaSpotPriceSource(asset="unibtc-universal-btc", currency="usd"), + ], + ), +) diff --git a/src/telliot_feeds/queries/price/spot_price.py b/src/telliot_feeds/queries/price/spot_price.py index b33be4c0..4785b9b4 100644 --- a/src/telliot_feeds/queries/price/spot_price.py +++ b/src/telliot_feeds/queries/price/spot_price.py @@ -101,6 +101,9 @@ "STONE/USD", "SUPEROETHB/ETH", "LSK/USD", + "UNIBTC/USD", + "SOLVBTC/USD", + "SOLVBTCBBN/USD", ] diff --git a/src/telliot_feeds/queries/query_catalog.py b/src/telliot_feeds/queries/query_catalog.py index a9d4bbe8..6b2fe034 100644 --- a/src/telliot_feeds/queries/query_catalog.py +++ b/src/telliot_feeds/queries/query_catalog.py @@ -668,3 +668,21 @@ title="LSK/USD spot price", q=SpotPrice(asset="lsk", currency="usd"), ) + +query_catalog.add_entry( + tag="unibtc-usd-spot", + title="UNIBTC/USD spot price", + q=SpotPrice(asset="unibtc", currency="usd"), +) + +query_catalog.add_entry( + tag="solvbtc-usd-spot", + title="SOLVBTC/USD spot price", + q=SpotPrice(asset="solvbtc", currency="usd"), +) + +query_catalog.add_entry( + tag="solvbtcbbn-usd-spot", + title="SOLVBTCBBN/USD spot price", + q=SpotPrice(asset="solvbtcbbn", currency="usd"), +) diff --git a/src/telliot_feeds/sources/lfj_source.py b/src/telliot_feeds/sources/lfj_source.py new file mode 100644 index 00000000..fde97c97 --- /dev/null +++ b/src/telliot_feeds/sources/lfj_source.py @@ -0,0 +1,150 @@ +from dataclasses import dataclass +from dataclasses import field +from typing import Any +from typing import List +from typing import Optional + +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__) + +LFJ_QUOTER_CONTRACT = "0x9A550a522BBaDFB69019b0432800Ed17855A51C3" +CONTRACT_ABI = [ + { + "inputs": [ + {"internalType": "address[]", "name": "route", "type": "address[]"}, + {"internalType": "uint128", "name": "amountIn", "type": "uint128"}, + ], + "name": "findBestPathFromAmountIn", + "outputs": [ + { + "components": [ + {"internalType": "address[]", "name": "route", "type": "address[]"}, + {"internalType": "address[]", "name": "pairs", "type": "address[]"}, + {"internalType": "uint256[]", "name": "binSteps", "type": "uint256[]"}, + {"internalType": "uint8[]", "name": "versions", "type": "uint8[]"}, # Enum is uint8 + {"internalType": "uint128[]", "name": "amounts", "type": "uint128[]"}, + {"internalType": "uint128[]", "name": "virtualAmountsWithoutSlippage", "type": "uint128[]"}, + {"internalType": "uint128[]", "name": "fees", "type": "uint128[]"}, + ], + "internalType": "tuple", # Struct is tuple in ABI + "name": "quote", + "type": "tuple", + } + ], + "stateMutability": "view", + "type": "function", + } +] +SOLVBTC_ROUTE = [ + "0xbc78D84Ba0c46dFe32cf2895a19939c86b81a777", # solvBTC + "0x152b9d0FdC40C096757F570A51E494bd4b943E50", # BTC.b + "0xB31f66AA3C1e785363F0875A1B74E27b85FD66c7", # AVAX + "0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E", # USDC +] + +SOLVBTCBBN_ROUTE = [ + "0xCC0966D8418d412c599A6421b760a847eB169A8c", # solvBTC.bbn + "0xbc78D84Ba0c46dFe32cf2895a19939c86b81a777", # solvBTC + "0x152b9d0FdC40C096757F570A51E494bd4b943E50", # BTC.b + "0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E", # USDC +] + + +class LFJPriceService(WebPriceService): + """Custom solvBTC Price Service""" + + def __init__(self, **kwargs: Any) -> None: + kwargs["name"] = "Custom solvBTC Price Service" + kwargs["url"] = "" + super().__init__(**kwargs) + self.cfg = TelliotConfig() + self.contract_address: Optional[str] = None + self.contract_abi: Optional[Any] = None + self.src_len: Optional[int] = None + self.route: Optional[List[Any]] = None + self.asset: Optional[str] = None + self.currency: Optional[str] = None + + def get_LFJ_quote(self, asset: str, currency: str) -> Optional[float]: + """call the quote function from LFG exchange. Routes are defined based on the pools + that show up on the LFJ front end for 1(token) vs usdc""" + # get endpoint + endpoint = self.cfg.endpoints.find(chain_id=43114) + if not endpoint: + logger.error("check avalanche RPC endpoint. unable to get LFJ quotes") + return None + ep = endpoint[0] + if not ep.connect(): + logger.error("Unable to connect to endpoint for LFJ source") + return None + w3 = ep._web3 + if w3 is None: + logger.error("Unable to get web3 for avalanche to get LFJ quotes") + return None + if asset == "solvbtc": + self.route = SOLVBTC_ROUTE + elif asset == "solvbtcbbn": + self.route = SOLVBTCBBN_ROUTE + else: + logger.error("No route list for getting LFJ price (asset not supported)") + return None + if currency != "usd": + logger.error("LFJ source is for usd pairs only!") + return None + # get solvbtc/eth ratio + price_quote = None + try: + route = self.route + amount_in = 1000000000000000000 + contract = w3.eth.contract(address=LFJ_QUOTER_CONTRACT, abi=CONTRACT_ABI) + contract_function = contract.functions.findBestPathFromAmountIn(route, amount_in) + data = contract_function.call() + response_int = data[5][3] + response_quote = w3.fromWei(response_int, "mwei") + price_quote = float(response_quote) + + except Exception as e: + logger.error(f"Error querying LFJ: {e}") + + return price_quote + + async def get_price(self, asset: str, currency: str) -> OptionalDataPoint[float]: + """This implementation gets the solvBTC/ETH ratio by checking the oracle + price from LFJ's price oracle contract + """ + asset = asset.lower() + currency = currency.lower() + lfj_quote = self.get_LFJ_quote(asset=asset, currency=currency) + logger.info(f"lfj quote for {asset}: {lfj_quote}") + if lfj_quote is None: + logger.error(f"lfj_quote is None for {asset} (check source)") + return None, None + + return lfj_quote, datetime_now_utc() + + +@dataclass +class LFJPriceSource(PriceSource): + """Gets data from LFJ contract""" + + asset: str = "" + currency: str = "" + service: LFJPriceService = field(default_factory=LFJPriceService, init=False) + + +if __name__ == "__main__": + import asyncio + + async def main() -> None: + source = LFJPriceSource(asset="solvbtcbbn", currency="usd") + v, _ = await source.fetch_new_datapoint() + print(v) + + asyncio.run(main()) diff --git a/src/telliot_feeds/sources/price/spot/coingecko.py b/src/telliot_feeds/sources/price/spot/coingecko.py index ce18d067..93cc80d2 100644 --- a/src/telliot_feeds/sources/price/spot/coingecko.py +++ b/src/telliot_feeds/sources/price/spot/coingecko.py @@ -92,6 +92,9 @@ "pufeth": "pufeth", "stone": "stakestone-ether", "superoethb": "super-oeth", + "unibtc": "universal-btc", + "solvbtc": "solv-btc", + "solvbtcbbn": "solv-protocol-solvbtc-bbn", } API_KEY = TelliotConfig().api_keys.find(name="coingecko")[0].key diff --git a/src/telliot_feeds/sources/price/spot/curvefiprice.py b/src/telliot_feeds/sources/price/spot/curvefiprice.py index 1abf01b2..6540e2f4 100644 --- a/src/telliot_feeds/sources/price/spot/curvefiprice.py +++ b/src/telliot_feeds/sources/price/spot/curvefiprice.py @@ -25,6 +25,7 @@ "rseth": "0xa1290d69c65a6fe4df752f95823fae25cb99e5a7", "ousd": "0x2a8e1e676ec238d8a992307b495b45b3feaa5e86", "pufeth": "0xd9a442856c234a39a81a089c06451ebaa4306a72", + "solvbtc": "0x7a56e1c57c7475ccf742a1832b028f0456652f97", } @@ -91,7 +92,7 @@ class CurveFiUSDPriceSource(PriceSource): import asyncio async def main() -> None: - source = CurveFiUSDPriceSource(asset="frxeth", currency="usd") + source = CurveFiUSDPriceSource(asset="solvbtcbbn", currency="usd") v, _ = await source.fetch_new_datapoint() print(v) diff --git a/src/telliot_feeds/sources/price/spot/pancakeswap.py b/src/telliot_feeds/sources/price/spot/pancakeswap.py index ec0fac5a..cac18dae 100644 --- a/src/telliot_feeds/sources/price/spot/pancakeswap.py +++ b/src/telliot_feeds/sources/price/spot/pancakeswap.py @@ -2,6 +2,10 @@ 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 @@ -12,51 +16,82 @@ logger = get_logger(__name__) pancakeswap_map = { "wbnb": "0xbb4cdb9cbd36b01bd1cbaebf2de08d9173bc095c", - "fuse": "0x5857c96dae9cf8511b08cb07f85753c472d36ea3", } +API_KEY = TelliotConfig().api_keys.find(name="thegraph")[0].key + class PancakeswapPriceService(WebPriceService): """Pancakeswap Price Service in USD and BNB""" def __init__(self, **kwargs: Any) -> None: kwargs["name"] = "Pancakeswap Price Service" - kwargs["url"] = "https://api.pancakeswap.info" + kwargs["url"] = "https://gateway-arbitrum.network.thegraph.com" super().__init__(**kwargs) async def get_price(self, asset: str, currency: str) -> OptionalDataPoint[float]: """Implement PriceServiceInterface - This implementation gets the price from the Pancakeswap API - Pankcakeswap official Github repo for API: - https://github.com/pancakeswap/pancake-info-api - + This implementation gets the price from a decentralized subgraph: + https://thegraph.com/explorer/subgraphs/Hv1GncLY5docZoGtXjo4kwbTvxm3MAhVZqBZE4sUT9eZ?view=Query&chain=arbitrum-one """ asset = asset.lower() - token_addr = pancakeswap_map.get(asset, None) - - if not token_addr: + token = pancakeswap_map.get(asset, None) + if not token: raise Exception("Asset not supported: {}".format(asset)) - request_url = f"/api/v2/tokens/{token_addr}" + query = "{bundles{id ethPriceUSD}token" + f'(id: "{token}")' + "{ derivedUSD } }" - d = self.get_url(request_url) + json_data = {"query": query} + + request_url = f"{self.url}/api/subgraphs/id/Hv1GncLY5docZoGtXjo4kwbTvxm3MAhVZqBZE4sUT9eZ" + + session = Session() + if API_KEY != "": + headers = {"Accepts": "application/json", "Authorization": f"Bearer {API_KEY}"} + session.headers.update(headers) + if API_KEY == "": + logger.warning("No Graph API key found for Uniswap prices!") + + 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} - if "error" in d: - logger.error(d) + except requests.exceptions.ConnectTimeout: + logger.warning("Timeout Error, No Uniswap prices retrieved (check thegraph api key)") + return None, None + + except Exception as e: + logger.warning(f"No prices retrieved from Uniswap: {e}") + return None, None + + if "error" in data: + logger.error(data) return None, None - elif "response" in d: - response = d["response"] - print(response) + if currency.lower() == "eth": + logger.info("Pancakeswap source is for usd pairs only") + return None, None + elif "response" in data: + response = data["response"] + logger.info(f"response: {response}") try: - price = response["data"]["price"] if currency.lower() == "usd" else response["data"]["price_BNB"] + price = response["data"]["token"]["derivedUSD"] + if price == 0.0: + msg = "Uniswap API not included, because price response is 0" + logger.warning(msg) + return None, None + else: + return price, datetime_now_utc() except KeyError as e: - msg = "Error parsing Pancakeswap API response: KeyError: {}".format(e) + msg = "Error parsing Pancakeswap response: KeyError: {}".format(e) logger.critical(msg) + return None, None else: raise Exception("Invalid response from get_url") @@ -69,3 +104,14 @@ class PancakeswapPriceSource(PriceSource): asset: str = "" currency: str = "" service: PancakeswapPriceService = field(default_factory=PancakeswapPriceService, init=False) + + +if __name__ == "__main__": + import asyncio + + async def main() -> None: + price_source = PancakeswapPriceSource(asset="wbnb", currency="usd") + price, timestamp = await price_source.fetch_new_datapoint() + print(price, timestamp) + + asyncio.run(main()) diff --git a/src/telliot_feeds/sources/price/spot/uniV3Optimism.py b/src/telliot_feeds/sources/price/spot/uniV3Optimism.py new file mode 100644 index 00000000..864c80c0 --- /dev/null +++ b/src/telliot_feeds/sources/price/spot/uniV3Optimism.py @@ -0,0 +1,123 @@ +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__) +# Add contract addresses for new assets to uniV3Optimism_map. +# Test that values work as expected before using this source in production! +uniV3Optimism_map = { + "op": "0x4200000000000000000000000000000000000042", +} + +API_KEY = TelliotConfig().api_keys.find(name="thegraph")[0].key + + +class UniV3OptimismPriceService(WebPriceService): + """Uniswap V3 on Optimism Price Service in USD and ETH""" + + def __init__(self, **kwargs: Any) -> None: + kwargs["name"] = "Uniswap V3 on Optimism Price Service" + kwargs["url"] = "https://gateway-arbitrum.network.thegraph.com" + 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 Uniswap V3 on Optimism subgraph + https://docs.uniswap.org/sdk/subgraph/subgraph-examples + + """ + + asset = asset.lower() + + token = uniV3Optimism_map.get(asset, None) + if not token: + raise Exception("Asset not supported: {}".format(asset)) + + query = "{bundles{id ethPriceUSD}token" + f'(id: "{token}")' + "{ derivedETH } }" + + json_data = {"query": query} + + request_url = f"{self.url}/api/subgraphs/id/Cghf4LfVqPiFw6fp6Y5X5Ubc8UpmUhSfJL82zwiBFLaj" + + session = Session() + if API_KEY != "": + headers = {"Accepts": "application/json", "Authorization": f"Bearer {API_KEY}"} + session.headers.update(headers) + if API_KEY == "": + logger.warning("No Graph API key found for Uniswap prices!") + + 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} + + except requests.exceptions.ConnectTimeout: + logger.warning("Timeout Error, No Uniswap prices retrieved (check thegraph api key)") + return None, None + + except Exception as e: + logger.warning(f"No prices retrieved from Uniswap: {e}") + return None, None + + if "error" in data: + logger.error(data) + return None, None + + elif "response" in data: + response = data["response"] + + try: + ethprice = float(response["data"]["bundles"][0]["ethPriceUSD"]) + if asset.lower() == "eth": + token_data = 1 + elif currency.lower() == "eth": + ethprice = 1 + token_data = response["data"]["token"]["derivedETH"] + else: + token_data = response["data"]["token"]["derivedETH"] + price = ethprice * float(token_data) + if price == 0.0: + msg = "Uniswap API not included, because price response is 0" + logger.warning(msg) + return None, None + else: + return price, datetime_now_utc() + except KeyError as e: + msg = "Error parsing Uniswap V3 on Optimism response: KeyError: {}".format(e) + logger.critical(msg) + return None, None + + else: + raise Exception("Invalid response from get_url") + + +@dataclass +class UniV3OptimismPriceSource(PriceSource): + asset: str = "" + currency: str = "" + service: UniV3OptimismPriceService = field(default_factory=UniV3OptimismPriceService, init=False) + + +if __name__ == "__main__": + import asyncio + + async def main() -> None: + price_source = UniV3OptimismPriceSource(asset="reth", currency="eth") + price, timestamp = await price_source.fetch_new_datapoint() + print(price, timestamp) + + asyncio.run(main()) diff --git a/src/telliot_feeds/sources/price/spot/uniswapV3.py b/src/telliot_feeds/sources/price/spot/uniswapV3.py index 9ee16a27..1f1a5b55 100644 --- a/src/telliot_feeds/sources/price/spot/uniswapV3.py +++ b/src/telliot_feeds/sources/price/spot/uniswapV3.py @@ -54,9 +54,8 @@ def __init__(self, **kwargs: Any) -> None: async def get_price(self, asset: str, currency: str) -> OptionalDataPoint[float]: """Implement PriceServiceInterface - This implementation gets the price from the UniswapV3 subgraph - https://docs.uniswap.org/sdk/subgraph/subgraph-examples - + This implementation gets the price from a decentralized subgraph: + https://thegraph.com/explorer/subgraphs/5zvR82QoaXYFyDEKLZ9t6v9adgnptxYpKpSbxtgVENFV?view=Query&chain=arbitrum-one """ asset = asset.lower() diff --git a/src/telliot_feeds/sources/solvbtcbbn_source.py b/src/telliot_feeds/sources/solvbtcbbn_source.py new file mode 100644 index 00000000..a61e95f3 --- /dev/null +++ b/src/telliot_feeds/sources/solvbtcbbn_source.py @@ -0,0 +1,107 @@ +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__) + + +API_KEY = TelliotConfig().api_keys.find(name="thegraph")[0].key + + +class pancakePoolPriceService(WebPriceService): + """PancakeSwap Price Service for Pool Ratios""" + + def __init__(self, **kwargs: Any) -> None: + kwargs["name"] = "PancakeSwap Price Service" + kwargs["url"] = "https://gateway-arbitrum.network.thegraph.com" + 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 PancakeSwap subgraph using the pool query + https://docs.uniswap.org/sdk/subgraph/subgraph-examples + + """ + headers = {"Content-Type": "application/json", "Authorization": f"Bearer {API_KEY}"} + graphql_query = """ + { + pool(id: "0x5a5ca75147550079411f6f543b729a4beab4dfeb") { + token0Price + token1 { + derivedUSD + } + } + } + """ + + json_data = {"query": graphql_query} + request_url = f"{self.url}/api/subgraphs/id/Hv1GncLY5docZoGtXjo4kwbTvxm3MAhVZqBZE4sUT9eZ" + + 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} + + except requests.exceptions.ConnectTimeout: + logger.warning("Timeout Error, No pool prices retrieved from Uniswap") + return None, None + + except Exception: + logger.warning("No pool prices retrieved from Uniswap") + return None, None + + if "error" in data: + logger.error(data) + return None, None + + elif "response" in data: + response_data = data["response"] + try: + pool_data = response_data["data"]["pool"] + token0_price = pool_data["token0Price"] + solvbtc_usd_price = pool_data["token1"]["derivedUSD"] + token_price = float(token0_price) * float(solvbtc_usd_price) + return token_price, datetime_now_utc() + except KeyError as e: + msg = "Error parsing Pancake pool response: KeyError: {}".format(e) + logger.critical(msg) + return None, None + + else: + raise Exception("Invalid response from get_url") + + +@dataclass +class pancakePoolPriceSource(PriceSource): + asset: str = "" + currency: str = "" + service: pancakePoolPriceService = field(default_factory=pancakePoolPriceService, init=False) + + +if __name__ == "__main__": + import asyncio + + async def main() -> None: + price_source = pancakePoolPriceSource(asset="solvbtcbbn", currency="usd") + price, timestamp = await price_source.fetch_new_datapoint() + print(price, timestamp) + + asyncio.run(main()) diff --git a/tests/feeds/test_op_usd_feed.py b/tests/feeds/test_op_usd_feed.py index 2888c7c2..096be44c 100644 --- a/tests/feeds/test_op_usd_feed.py +++ b/tests/feeds/test_op_usd_feed.py @@ -12,7 +12,7 @@ async def test_op_asset_price_feed(caplog): assert v is not None assert v > 0 - assert "sources used in aggregate: 3" in caplog.text.lower() + assert "sources used in aggregate: 4" in caplog.text.lower() print(f"OP/USD Price: {v}") # Get list of data sources from sources dict diff --git a/tests/feeds/test_solvbtc_usd_feed.py b/tests/feeds/test_solvbtc_usd_feed.py new file mode 100644 index 00000000..ff30ad58 --- /dev/null +++ b/tests/feeds/test_solvbtc_usd_feed.py @@ -0,0 +1,22 @@ +import statistics + +import pytest + +from telliot_feeds.feeds.solvbtc_usd_feed import solvbtc_usd_median_feed + + +@pytest.mark.asyncio +async def test_solvbtc_usd_median_feed(caplog): + """Retrieve median solvbtc/usd price.""" + v, _ = await solvbtc_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"solvbtc/usd Price: {v}") + + # Get list of data sources from sources dict + source_prices = [source.latest[0] for source in solvbtc_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 diff --git a/tests/feeds/test_solvbtcbbn_usd_feed.py b/tests/feeds/test_solvbtcbbn_usd_feed.py new file mode 100644 index 00000000..a01a8527 --- /dev/null +++ b/tests/feeds/test_solvbtcbbn_usd_feed.py @@ -0,0 +1,22 @@ +import statistics + +import pytest + +from telliot_feeds.feeds.solvbtcbbn_usd_feed import solvbtcbbn_usd_median_feed + + +@pytest.mark.asyncio +async def test_solvbtcbbn_usd_median_feed(caplog): + """Retrieve median solvbtcbbn/usd price.""" + v, _ = await solvbtcbbn_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"solvbtcbbn/usd Price: {v}") + + # Get list of data sources from sources dict + source_prices = [source.latest[0] for source in solvbtcbbn_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 diff --git a/tests/feeds/test_unibtc_usd_feed.py b/tests/feeds/test_unibtc_usd_feed.py new file mode 100644 index 00000000..a61931c2 --- /dev/null +++ b/tests/feeds/test_unibtc_usd_feed.py @@ -0,0 +1,22 @@ +import statistics + +import pytest + +from telliot_feeds.feeds.unibtc_usd_feed import unibtc_usd_median_feed + + +@pytest.mark.asyncio +async def test_unibtc_usd_median_feed(caplog): + """Retrieve median unibtc/usd price.""" + v, _ = await unibtc_usd_median_feed.source.fetch_new_datapoint() + + assert v is not None + assert v > 0 + assert "sources used in aggregate: 2" in caplog.text.lower() + print(f"unibtc/usd Price: {v}") + + # Get list of data sources from sources dict + source_prices = [source.latest[0] for source in unibtc_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