Skip to content

Commit

Permalink
feat: add USDPLUS support (#228)
Browse files Browse the repository at this point in the history
* feat: usdplus and some refacto

* fix: ekubo hopping w/ usdplus + defillama fetcher

* Update fetcher_test.py

* Update config.example.yaml

* fix: cleanup
  • Loading branch information
EvolveArt authored Nov 22, 2024
1 parent 149007e commit e6762c0
Show file tree
Hide file tree
Showing 10 changed files with 170 additions and 23 deletions.
4 changes: 2 additions & 2 deletions infra/price-pusher/config/config.mainnet.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
- ZEND/USD
- NSTR/USD
- EKUBO/USD
- BROTHER/USD
- BROTHER/USDPLUS
time_difference: 600
price_deviation: 0.025

Expand All @@ -30,5 +30,5 @@
- ETH/USD
- BTC/USDT
- ETH/USDT
time_difference: 600
time_difference: 3000
price_deviation: 0.05
2 changes: 1 addition & 1 deletion infra/price-pusher/config/config.sepolia.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
- ZEND/USD
- NSTR/USD
- EKUBO/USD
- BROTHER/USD
- BROTHER/USDPLUS
- LUSD/USD
time_difference: 120
price_deviation: 0.005
Expand Down
2 changes: 1 addition & 1 deletion pragma-sdk/pragma_sdk/common/fetchers/fetchers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from pragma_sdk.common.fetchers.fetchers.bybit import BybitFetcher
from pragma_sdk.common.fetchers.fetchers.coinbase import CoinbaseFetcher
from pragma_sdk.common.fetchers.fetchers.defillama import DefillamaFetcher
from pragma_sdk.common.fetchers.fetchers.gecko import GeckoTerminalFetcher
from pragma_sdk.common.fetchers.fetchers.geckoterminal import GeckoTerminalFetcher
from pragma_sdk.common.fetchers.fetchers.huobi import HuobiFetcher
from pragma_sdk.common.fetchers.fetchers.indexcoop import IndexCoopFetcher
from pragma_sdk.common.fetchers.fetchers.kucoin import KucoinFetcher
Expand Down
6 changes: 2 additions & 4 deletions pragma-sdk/pragma_sdk/common/fetchers/fetchers/defillama.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,7 @@


class DefillamaFetcher(FetcherInterfaceT):
BASE_URL: str = (
"https://coins.llama.fi/prices/current/coingecko:{pair_id}" "?searchWidth=15m"
)
BASE_URL: str = "https://coins.llama.fi/prices/current/coingecko:{pair_id}"
SOURCE: str = "DEFILLAMA"

async def fetch_pair(
Expand All @@ -28,7 +26,7 @@ async def fetch_pair(
return PublisherFetchError(
f"Unknown price pair, do not know how to query Coingecko for {pair.base_currency.id}"
)
if pair.quote_currency.id != "USD":
if pair.quote_currency.id not in ("USD", "USDPLUS"):
return await self.operate_usd_hop(pair, session)

url = self.format_url(pair)
Expand Down
4 changes: 2 additions & 2 deletions pragma-sdk/pragma_sdk/common/fetchers/fetchers/dexscreener.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ async def fetch_pair(
NOTE: The base currency being priced must have either a starknet_address
or an ethereum_address.
"""
if pair.quote_currency.id != "USD":
if pair.quote_currency.id not in ("USD", "USDPLUS"):
return PublisherFetchError(f"No data found for {pair} from Dexscreener")
if (pair.base_currency.ethereum_address == 0) and (
pair.base_currency.starknet_address == 0
Expand Down Expand Up @@ -111,7 +111,7 @@ def format_url( # type: ignore[override]
"""
Format the URL to fetch in order to retrieve the price for a pair.
"""
if pair.base_currency.ethereum_address is not None:
if pair.base_currency.ethereum_address not in (None, 0):
base_address = f"{pair.base_currency.ethereum_address:#0{42}x}"
else:
base_address = f"{pair.base_currency.starknet_address:#0{66}x}"
Expand Down
31 changes: 20 additions & 11 deletions pragma-sdk/pragma_sdk/common/fetchers/fetchers/ekubo.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,7 @@ class EkuboFetcher(FetcherInterfaceT):
publisher: str
price_fetcher_contract: int
hop_handler: HopHandler = HopHandler(
hopped_currencies={
"USD": "USDC",
}
hopped_currencies={"USD": "USDC", "USDPLUS": "USDC"}
)

def __init__(
Expand Down Expand Up @@ -261,18 +259,29 @@ async def _adapt_back_hopped_pair(
At the end, we return the original Pair before hop and the price.
"""
for asset, hopped_to in self.hop_handler.hopped_currencies.items():
if hopped_to == pair.quote_currency.id:
hop: Tuple[str, str] = (asset, hopped_to)
break
# For USDPLUS, use USD prices directly
requested_quote = self.pairs[0].quote_currency.id
lookup_quote = "USD" if requested_quote == "USDPLUS" else requested_quote

hop_quote_pair = Pair.from_tickers(pair.quote_currency.id, hop[0])
hop_quote_pair = Pair.from_tickers(pair.quote_currency.id, lookup_quote)
hop_price = hop_prices.get(hop_quote_pair)

if hop_price is None:
raise ValueError("Could not find hop price. Should never happen.")
# Try reverse pair
reverse_pair = Pair.from_tickers(lookup_quote, pair.quote_currency.id)
reverse_price = hop_prices.get(reverse_pair)
if reverse_price:
hop_price = 1 / reverse_price
else:
raise ValueError(
f"No valid hop price found for {hop_quote_pair} or {reverse_pair}"
)

# Create the final pair with the originally requested quote currency
new_pair = Pair.from_tickers(pair.base_currency.id, requested_quote)
final_price = price * hop_price

new_pair = Pair.from_tickers(pair.base_currency.id, hop[0])
return (new_pair, price * hop_price)
return (new_pair, final_price)

def _handle_error_status(
self, status: EkuboStatus, pair: Pair
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from typing import Any, Dict, List, Optional

from aiohttp import ClientSession
import aiohttp

from pragma_sdk.common.configs.asset_config import AssetConfig
from pragma_sdk.common.types.currency import Currency
Expand Down Expand Up @@ -82,7 +83,7 @@ class GeckoTerminalFetcher(FetcherInterfaceT):
async def fetch_pair(
self, pair: Pair, session: ClientSession
) -> SpotEntry | PublisherFetchError:
if pair.quote_currency.id != "USD":
if pair.quote_currency.id not in ("USD", "USDPLUS"):
return await self.operate_usd_hop(pair, session)
pool = ASSET_MAPPING.get(pair.base_currency.id)
if pool is None:
Expand Down
7 changes: 6 additions & 1 deletion pragma-sdk/pragma_sdk/supported_assets.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@
ticker: 'USD'
abstract: true

- name: 'US Dollar (18 decimals)'
decimals: 18
ticker: 'USDPLUS'
abstract: true

- name: 'Euro'
decimals: 8
ticker: 'EUR'
Expand Down Expand Up @@ -245,4 +250,4 @@
decimals: 18
ticker: 'BROTHER'
coingecko_id: 'starknet-brother'
starknet_address: '0x3b405a98c9e795d427fe82cdeeeed803f221b52471e3a757574a2b4180793ee'
starknet_address: '0x03b405a98c9e795d427fe82cdeeeed803f221b52471e3a757574a2b4180793ee'
133 changes: 133 additions & 0 deletions pragma-sdk/tests/fetcher_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import asyncio
import argparse
import aiohttp
from datetime import datetime
from typing import Type, Optional

from pragma_sdk.common.types.pair import Pair
from pragma_sdk.common.types.currency import Currency
from pragma_sdk.common.configs.asset_config import AssetConfig
from pragma_sdk.common.types.entry import SpotEntry
from pragma_sdk.common.exceptions import PublisherFetchError
from pragma_sdk.common.fetchers.interface import FetcherInterfaceT
from pragma_sdk.common.utils import felt_to_str

"""
Smol script to test any fetcher easily
e.g
```
python fetcher_test.py DefillamaFetcher BROTHER USDPLUS
```
"""
async def test_fetcher(
fetcher_class: Type[FetcherInterfaceT],
base_currency: str,
quote_currency: str,
api_key: Optional[str] = None,
) -> None:
"""
Test a price fetcher for a specific currency pair.
Args:
fetcher_class: The fetcher class to test
base_currency: Base currency ticker (e.g., "BTC")
quote_currency: Quote currency ticker (e.g., "USD")
api_key: Optional API key for the fetcher
"""
# Create the currency pair
base = Currency.from_asset_config(AssetConfig.from_ticker(base_currency))
quote = Currency.from_asset_config(AssetConfig.from_ticker(quote_currency))
pair = Pair(base, quote)

# Initialize the fetcher
fetcher = fetcher_class(pairs=[pair], publisher="TEST")

# Set API key if provided
if api_key and hasattr(fetcher, "headers"):
fetcher.headers["Authorization"] = f"Bearer {api_key}"

print(f"\nTesting {fetcher.__class__.__name__} for pair: {pair}")
print("-" * 50)

try:
async with aiohttp.ClientSession() as session:
start_time = datetime.now()
results = await fetcher.fetch(session)
result = results[0]
end_time = datetime.now()

if isinstance(result, SpotEntry):
print("✅ Successfully fetched price:")
print(f" Price: {result.price}")
print(
f" Human readable price: {result.price / (10 ** pair.decimals())}"
)
print(f" Timestamp: {result.base.timestamp}")
print(
f" Human readable time: {datetime.fromtimestamp(result.base.timestamp)}"
)
print(f" Volume: {result.volume}")
print(f" Source: {felt_to_str(result.base.source)}")
print(f" Publisher: {felt_to_str(result.base.publisher)}")
print(f" Fetch time: {(end_time - start_time).total_seconds():.3f}s")
elif isinstance(result, PublisherFetchError):
print("❌ Error fetching price:")
print(f" Error message: {str(result)}")
else:
print("❌ Unexpected result type:")
print(f" {type(result)}: {result}")

except Exception as e:
print("❌ Exception occurred:")
print(f" {type(e).__name__}: {str(e)}")


def main():
parser = argparse.ArgumentParser(
description="Test a price fetcher for a specific currency pair"
)
parser.add_argument(
"fetcher", type=str, help="Fetcher class name (e.g., GeckoTerminalFetcher)"
)
parser.add_argument("base", type=str, help="Base currency ticker (e.g., BTC)")
parser.add_argument("quote", type=str, help="Quote currency ticker (e.g., USD)")
parser.add_argument(
"--api-key", type=str, help="API key for the fetcher", default=None
)

args = parser.parse_args()

# Import the fetcher class dynamically
try:
# This assumes the fetcher is in the same directory
# You might need to modify this to import from different locations
module_name = args.fetcher.lower()
if module_name.endswith("fetcher"):
module_name = module_name[:-7]

fetcher_module = __import__(
f"pragma_sdk.common.fetchers.fetchers.{module_name}",
fromlist=[args.fetcher],
)
fetcher_class = getattr(fetcher_module, args.fetcher)

asyncio.run(
test_fetcher(
fetcher_class=fetcher_class,
base_currency=args.base,
quote_currency=args.quote,
api_key=args.api_key,
)
)

except ImportError as e:
print(f"❌ Could not import fetcher class '{args.fetcher}', {e}")
except AttributeError:
print(f"❌ Could not find fetcher class '{args.fetcher}' in module")
except Exception as e:
print(f"❌ Error: {type(e).__name__}: {str(e)}")


if __name__ == "__main__":
main()
1 change: 1 addition & 0 deletions price-pusher/config/config.example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
spot:
- BTC/USD
- ETH/USD
- BROTHER/USDPLUS

time_difference: 1
price_deviation: 0.025

0 comments on commit e6762c0

Please sign in to comment.