File size: 6,507 Bytes
97873ec
76f2683
fcfadb5
 
 
97873ec
fd31489
476cee0
c4fd2ca
fd31489
c4fd2ca
fd31489
19f368e
fd31489
7731384
76f2683
fcfadb5
 
 
 
 
 
 
 
 
 
76f2683
 
fd31489
7731384
c4fd2ca
 
fcfadb5
431e338
19f368e
fd31489
76f2683
19f368e
476cee0
fcfadb5
 
476cee0
fcfadb5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c4fd2ca
fcfadb5
 
 
 
 
 
 
 
 
 
 
431e338
fcfadb5
 
 
 
 
 
 
 
431e338
fcfadb5
 
19f368e
431e338
c4fd2ca
fcfadb5
 
 
 
c4fd2ca
 
fcfadb5
 
 
 
c4fd2ca
fcfadb5
431e338
 
76f2683
fcfadb5
c4fd2ca
 
76f2683
 
c4fd2ca
76f2683
 
fcfadb5
431e338
76f2683
431e338
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
"""
A professional-grade, multi-asset, multi-oracle price engine.
v13.0 FINAL: This version uses FOUR independent data sources and calculates a
resilient median price to be immune to single-source API failures. This is the
most robust and definitive version.
"""
import asyncio
import logging
from typing import Dict, Optional, List
import httpx
import statistics

logger = logging.getLogger(__name__)

# --- CONFIGURATION ---
ASSET_CONFIG = {
    "BTC": {"pyth_id": "e62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415B43", "coingecko_id": "bitcoin", "coincap_id": "bitcoin", "cryptocompare_id": "BTC"},
    "ETH": {"pyth_id": "ff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace", "coingecko_id": "ethereum", "coincap_id": "ethereum", "cryptocompare_id": "ETH"},
    "SOL": {"pyth_id": "ef0d8b6fda2ceba41da15d4095d1da392a0d2f8ed0c6c7bc0f4cfac8c280b56d", "coingecko_id": "solana", "coincap_id": "solana", "cryptocompare_id": "SOL"},
    "XRP": {"pyth_id": "02a01e69981d314fd8a723be08253181e53b4945b4bf376d15a51980a37330c3", "coingecko_id": "ripple", "coincap_id": "xrp", "cryptocompare_id": "XRP"},
    "DOGE": {"pyth_id": "042f02faf4229babc62635593855b6a383d6a4a2a1b9b9a7c385a4a50b86a345", "coingecko_id": "dogecoin", "coincap_id": "dogecoin", "cryptocompare_id": "DOGE"},
    "ADA": {"pyth_id": "34f544985c7943c093b5934963505a767f4749445244a852654c6017b28091ea", "coingecko_id": "cardano", "coincap_id": "cardano", "cryptocompare_id": "ADA"},
    "AVAX": {"pyth_id": "0x141f2a3c34c8035443a01d64380b52207991b16c14c5145f617eb578a994753c", "coingecko_id": "avalanche-2", "coincap_id": "avalanche", "cryptocompare_id": "AVAX"},
    "LINK": {"pyth_id": "0x63f4f4755a5a67c64c781d45763b33a72666a15e6b91c0fbdf3b2f205d5a6b01", "coingecko_id": "chainlink", "coincap_id": "chainlink", "cryptocompare_id": "LINK"},
    "DOT": {"pyth_id": "0x00a896677493a74421b33362a7447785b13612f0e340d418641a33716a5067a3", "coingecko_id": "polkadot", "coincap_id": "polkadot", "cryptocompare_id": "DOT"},
    "MATIC": {"pyth_id": "0x737ac3c13709b45da8128ff9e1058a984f86a048035656111b8a365e4921648a", "coingecko_id": "matic-network", "coincap_id": "polygon", "cryptocompare_id": "MATIC"},
}

class PriceFetcher:
    PYTH_URL = "https://hermes.pyth.network/v2/price_feeds"
    COINGECKO_URL = "https://api.coingecko.com/api/v3/simple/price"
    COINCAP_URL = "https://api.coincap.io/v2/assets"
    CRYPTOCOMPARE_URL = "https://min-api.cryptocompare.com/data/pricemulti"

    def __init__(self, client: httpx.AsyncClient):
        self.client = client
        self._prices: Dict[str, Dict[str, Optional[float]]] = {}
        self._lock = asyncio.Lock()

    async def _fetch_pyth(self, ids: List[str]) -> Dict[str, float]:
        params = [("ids[]", f"0x{pid}") for pid in ids]
        try:
            resp = await self.client.get(self.PYTH_URL, params=params, timeout=10)
            resp.raise_for_status()
            id_map = {f"0x{v['pyth_id']}": k for k, v in ASSET_CONFIG.items()}
            return {id_map[item['id']]: int(item['price']['price']) / (10 ** abs(int(item['price']['expo']))) for item in resp.json() if item['id'] in id_map}
        except Exception as e:
            logger.error(f"❌ Oracle Error (Pyth): {e}")
            return {}

    async def _fetch_coingecko(self, ids: List[str]) -> Dict[str, float]:
        params = {"ids": ",".join(ids), "vs_currencies": "usd"}
        try:
            resp = await self.client.get(self.COINGECKO_URL, params=params, timeout=10)
            resp.raise_for_status()
            id_map = {v['coingecko_id']: k for k, v in ASSET_CONFIG.items()}
            return {id_map[cg_id]: prices['usd'] for cg_id, prices in resp.json().items()}
        except Exception as e:
            logger.error(f"❌ Oracle Error (CoinGecko): {e}")
            return {}

    async def _fetch_coincap(self, ids: List[str]) -> Dict[str, float]:
        # --- THE CRITICAL FIX IS HERE ---
        # CoinCap's multi-asset endpoint does not use 'ids', it returns all. We filter.
        try:
            resp = await self.client.get(self.COINCAP_URL, params={"limit": 200}, timeout=10)
            resp.raise_for_status()
            id_map = {v['coincap_id']: k for k, v in ASSET_CONFIG.items()}
            return {id_map[item['id']]: float(item['priceUsd']) for item in resp.json().get('data', []) if item['id'] in id_map}
        except Exception as e:
            logger.error(f"❌ Oracle Error (CoinCap): {e}")
            return {}

    async def _fetch_cryptocompare(self, ids: List[str]) -> Dict[str, float]:
        params = {"fsyms": ",".join(ids), "tsyms": "USD"}
        try:
            resp = await self.client.get(self.CRYPTOCOMPARE_URL, params=params, timeout=10)
            resp.raise_for_status()
            data = resp.json()
            id_map = {v['cryptocompare_id']: k for k, v in ASSET_CONFIG.items()}
            return {id_map[cc_id]: price_data['USD'] for cc_id, price_data in data.items() if 'USD' in price_data}
        except Exception as e:
            logger.error(f"❌ Oracle Error (CryptoCompare): {e}")
            return {}

    async def update_prices_async(self):
        # Prepare asset ID maps for each source
        pyth_ids = [v['pyth_id'] for v in ASSET_CONFIG.values()]
        coingecko_ids = [v['coingecko_id'] for v in ASSET_CONFIG.values()]
        coincap_ids = [v['coincap_id'] for v in ASSET_CONFIG.values()]
        cryptocompare_ids = [v['cryptocompare_id'] for v in ASSET_CONFIG.values()]

        tasks = [
            self._fetch_pyth(pyth_ids),
            self._fetch_coingecko(coingecko_ids),
            self._fetch_coincap(coincap_ids),
            self._fetch_cryptocompare(cryptocompare_ids),
        ]
        pyth_prices, coingecko_prices, coincap_prices, cryptocompare_prices = await asyncio.gather(*tasks)
        
        async with self._lock:
            for symbol in ASSET_CONFIG.keys():
                valid_agg_prices = [p for p in [coingecko_prices.get(symbol), coincap_prices.get(symbol), cryptocompare_prices.get(symbol)] if p]
                median_agg_price = statistics.median(valid_agg_prices) if valid_agg_prices else None
                
                self._prices[symbol] = {
                    "pyth": pyth_prices.get(symbol),
                    "chainlink_agg": median_agg_price
                }
        
        logger.info("βœ… Multi-Source Prices Updated")

    def get_all_prices(self) -> Dict[str, Dict[str, Optional[float]]]:
        return self._prices.copy()