Repository: tellor-io/telliot-feeds (language: python)

Issue:
Add Maverick Exchange as a Source (for SWETH/USD) if Possible
Generally, we need to find more / better sources to use for SWETH/USD because Tellor will be integrated as an oracle for that spot price feed in RAFT's lending protocol. We are alreay using Uniswap (and coinGecko). Here is a link to RedStone oracles SWETH feed: https://app.redstone.finance/#/app/token/SWETH

It looks like they are using Maverick:
https://app.mav.xyz/?chain=1&tokenA=ETH&tokenB=0xf951E335afb289353dc249e82926178EaC7DEd78

and Balancer:
https://app.balancer.fi/#/ethereum/pool/0x02d928e68d8f10c0358566152677db51e1e2dc8c00000000000000000000051e

Let me know if any more information would be helpful for this 🙏


Discussion / hints:

Fix patch:
diff --git a/src/telliot_feeds/feeds/sweth_usd_feed.py b/src/telliot_feeds/feeds/sweth_usd_feed.py
index 26b5c52b..ed01f983 100644
--- a/src/telliot_feeds/feeds/sweth_usd_feed.py
+++ b/src/telliot_feeds/feeds/sweth_usd_feed.py
@@ -3,6 +3,9 @@
 from telliot_feeds.sources.price.spot.coingecko import CoinGeckoSpotPriceSource
 from telliot_feeds.sources.price.spot.uniswapV3 import UniswapV3PriceSource
 from telliot_feeds.sources.price_aggregator import PriceAggregator
+from telliot_feeds.sources.sweth_source import swETHMaverickSpotPriceSource
+from telliot_feeds.sources.sweth_source import swETHSpotPriceSource
+
 
 sweth_usd_median_feed = DataFeed(
     query=SpotPrice(asset="SWETH", currency="USD"),
@@ -11,8 +14,23 @@
         currency="usd",
         algorithm="median",
         sources=[
+            swETHSpotPriceSource(asset="sweth", currency="usd"),
+            swETHMaverickSpotPriceSource(asset="sweth", currency="usd"),
             CoinGeckoSpotPriceSource(asset="sweth", currency="usd"),
             UniswapV3PriceSource(asset="sweth", currency="usd"),
         ],
     ),
 )
+if __name__ == "__main__":
+    import asyncio
+
+    async def main() -> None:
+        source = swETHMaverickSpotPriceSource(asset="sweth", currency="usd")
+        v, _ = await source.fetch_new_datapoint()
+        print(v)
+
+        source = swETHSpotPriceSource(asset="sweth", currency="usd")
+        v, _ = await source.fetch_new_datapoint()
+        print(v)
+
+    asyncio.run(main())
diff --git a/src/telliot_feeds/sources/sweth_source.py b/src/telliot_feeds/sources/sweth_source.py
new file mode 100644
index 00000000..77340641
--- /dev/null
+++ b/src/telliot_feeds/sources/sweth_source.py
@@ -0,0 +1,100 @@
+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 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__)
+
+MAVERICK_CONTRACT = "0x9980ce3b5570e41324904f46A06cE7B466925E23"
+SWETH_CONTRACT = "0xf951E335afb289353dc249e82926178EaC7DEd78"
+
+
+class swETHSpotPriceService(WebPriceService):
+    """Custom swETH Price Service"""
+
+    def __init__(self, **kwargs: Any) -> None:
+        kwargs["name"] = "Custom swETH Price Service"
+        kwargs["url"] = ""
+        super().__init__(**kwargs)
+        self.cfg = TelliotConfig()
+        self.contract: Optional[str] = None
+        self.calldata: Optional[str] = None
+
+    def get_sweth_eth_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 sweth_eth_ratio")
+            return None
+        ep = endpoint[0]
+        if not ep.connect():
+            logger.error("Unable to connect endpoint for mainnet to get sweth_eth_ratio")
+            return None
+        w3 = ep._web3
+        if w3 is None:
+            logger.error("Unable to get web3 for mainnet to get sweth_eth_ratio")
+            return None
+        # get ratio
+        sweth_eth_ratio_bytes = w3.eth.call({"to": self.contract, "data": self.calldata})
+
+        sweth_eth_ratio_decoded = w3.toInt(sweth_eth_ratio_bytes)
+        sweth_eth_ratio = w3.fromWei(sweth_eth_ratio_decoded, "ether")
+        # Maverick AMM uses square root of ratio
+        if self.contract == MAVERICK_CONTRACT:
+            sweth_eth_ratio = sweth_eth_ratio**2
+        logger.debug(f"sweth_eth_ratio: {sweth_eth_ratio}")
+        return float(sweth_eth_ratio)
+
+    async def get_price(self, asset: str, currency: str) -> OptionalDataPoint[float]:
+        """This implementation gets the median price of eth in usd and
+        converts the sweth/eth ratio to get sweth/usd price
+        """
+        asset = asset.lower()
+        currency = currency.lower()
+
+        sweth_eth_ratio = self.get_sweth_eth_ratio()
+        if asset == "sweth" and currency == "eth":
+            return sweth_eth_ratio, datetime_now_utc()
+
+        if sweth_eth_ratio is None:
+            logger.error("Unable to get sweth_eth_ratio")
+            return None, None
+        from telliot_feeds.feeds.eth_usd_feed import eth_usd_median_feed
+
+        source = eth_usd_median_feed.source
+
+        eth_price, timestamp = await source.fetch_new_datapoint()
+        if eth_price is None:
+            logger.error("Unable to get eth/usd price")
+            return None, None
+        return sweth_eth_ratio * eth_price, timestamp
+
+
+@dataclass
+class swETHSpotPriceSource(PriceSource):
+    asset: str = ""
+    currency: str = ""
+    service: swETHSpotPriceService = field(default_factory=swETHSpotPriceService, init=False)
+
+    def __post_init__(self) -> None:
+        self.service.contract = SWETH_CONTRACT
+        self.service.calldata = "0xd68b2cb6"
+
+
+@dataclass
+class swETHMaverickSpotPriceSource(PriceSource):
+    asset: str = ""
+    currency: str = ""
+    service: swETHSpotPriceService = field(default_factory=swETHSpotPriceService)
+
+    def __post_init__(self) -> None:
+        self.service.contract = MAVERICK_CONTRACT
+        self.service.calldata = "0x91c0914e000000000000000000000000817e8c9a99db98082ca187e4f80498586bf6bc1b"


Test patch:
diff --git a/tests/feeds/test_sweth_usd_feed.py b/tests/feeds/test_sweth_usd_feed.py
index 5fe18876..32785c15 100644
--- a/tests/feeds/test_sweth_usd_feed.py
+++ b/tests/feeds/test_sweth_usd_feed.py
@@ -12,7 +12,7 @@ async def test_sweth_usd_median_feed(caplog):
 
     assert v is not None
     assert v > 0
-    assert "sources used in aggregate: 2" in caplog.text.lower()
+    assert "sources used in aggregate: 4" in caplog.text.lower()
     print(f"SWETH/USD Price: {v}")
 
     # Get list of data sources from sources dict
diff --git a/tests/sources/test_spot_price_sources.py b/tests/sources/test_spot_price_sources.py
index 7367e919..35d1eee0 100644
--- a/tests/sources/test_spot_price_sources.py
+++ b/tests/sources/test_spot_price_sources.py
@@ -24,6 +24,7 @@
     PancakeswapPriceService,
 )
 from telliot_feeds.sources.price.spot.uniswapV3 import UniswapV3PriceService
+from telliot_feeds.sources.sweth_source import swETHSpotPriceService
 
 
 service = {
@@ -39,6 +40,7 @@
     "bitfinex": BitfinexSpotPriceService(),
     "coinpaprika": CoinpaprikaSpotPriceService(),
     "curvefi": CurveFinanceSpotPriceService(),
+    "sweth": swETHSpotPriceService(),
 }
 
 
@@ -305,3 +307,12 @@ async def test_curvefi():
     validate_price(v, t)
     assert v is not None
     assert t is not None
+
+
+@pytest.mark.asyncio
+async def test_sweth_source():
+    """Test swETH price service"""
+    v, t = await get_price("sweth", "usd", service["sweth"])
+    validate_price(v, t)
+    assert v is not None
+    assert t is not None