diff --git a/electrum/exchange_rate.py b/electrum/exchange_rate.py index e2c4add95..c9a7cc6d5 100644 --- a/electrum/exchange_rate.py +++ b/electrum/exchange_rate.py @@ -48,12 +48,17 @@ def to_decimal(x: Union[str, float, int, Decimal]) -> Decimal: return Decimal(str(x)) +POLL_PERIOD_SPOT_RATE = 150 # approx. every 2.5 minutes, try to refresh spot price +EXPIRY_SPOT_RATE = 600 # spot price becomes stale after 10 minutes + + class ExchangeBase(Logger): def __init__(self, on_quotes, on_history): Logger.__init__(self) self._history = {} # type: Dict[str, Dict[str, str]] - self.quotes = {} # type: Dict[str, Optional[Decimal]] + self._quotes = {} # type: Dict[str, Optional[Decimal]] + self._quotes_timestamp = 0 # type: Union[int, float] self.on_quotes = on_quotes self.on_history = on_history @@ -89,16 +94,15 @@ class ExchangeBase(Logger): async def update_safe(self, ccy: str) -> None: try: self.logger.info(f"getting fx quotes for {ccy}") - self.quotes = await self.get_rates(ccy) - assert all(isinstance(rate, (Decimal, type(None))) for rate in self.quotes.values()), \ - f"fx rate must be Decimal, got {self.quotes}" + self._quotes = await self.get_rates(ccy) + assert all(isinstance(rate, (Decimal, type(None))) for rate in self._quotes.values()), \ + f"fx rate must be Decimal, got {self._quotes}" + self._quotes_timestamp = time.time() self.logger.info("received fx quotes") except (aiohttp.ClientError, asyncio.TimeoutError) as e: self.logger.info(f"failed fx quotes: {repr(e)}") - self.quotes = {} except Exception as e: self.logger.exception(f"failed fx quotes: {repr(e)}") - self.quotes = {} self.on_quotes() def read_historical_rates(self, ccy: str, cache_dir: str) -> Optional[dict]: @@ -167,6 +171,16 @@ class ExchangeBase(Logger): rates = await self.get_rates('') return sorted([str(a) for (a, b) in rates.items() if b is not None and len(a)==3]) + def get_cached_spot_quote(self, ccy: str) -> Decimal: + """Returns the cached exchange rate as a Decimal""" + rate = self._quotes.get(ccy) + if rate is None: + return Decimal('NaN') + if self._quotes_timestamp + EXPIRY_SPOT_RATE < time.time(): + # Our rate is stale. Probably better to return no rate than an incorrect one. + return Decimal('NaN') + return Decimal(rate) + class BitcoinAverage(ExchangeBase): # note: historical rates used to be freely available @@ -429,7 +443,7 @@ class Biscoint(ExchangeBase): class Walltime(ExchangeBase): async def get_rates(self, ccy): - json = await self.get_json('s3.amazonaws.com', + json = await self.get_json('s3.amazonaws.com', '/data-production-walltime-info/production/dynamic/walltime-info.json') return {'BRL': to_decimal(json['BRL_XBT']['last_inexact'])} @@ -542,9 +556,9 @@ class FxThread(ThreadJob, EventListener): async def run(self): while True: - # approx. every 2.5 minutes, refresh spot price + # every few minutes, refresh spot price try: - async with timeout_after(150): + async with timeout_after(POLL_PERIOD_SPOT_RATE): await self._trigger.wait() self._trigger.clear() # we were manually triggered, so get historical rates @@ -583,7 +597,7 @@ class FxThread(ThreadJob, EventListener): def set_fiat_address_config(self, b): self.config.set_key('fiat_address', bool(b)) - def get_currency(self): + def get_currency(self) -> str: '''Use when dynamic fetching is needed''' return self.config.get("currency", DEFAULT_CURRENCY) @@ -625,10 +639,7 @@ class FxThread(ThreadJob, EventListener): """Returns the exchange rate as a Decimal""" if not self.is_enabled(): return Decimal('NaN') - rate = self.exchange.quotes.get(self.ccy) - if rate is None: - return Decimal('NaN') - return Decimal(rate) + return self.exchange.get_cached_spot_quote(self.ccy) def format_amount(self, btc_balance, *, timestamp: int = None) -> str: if timestamp is None: @@ -667,7 +678,7 @@ class FxThread(ThreadJob, EventListener): # Frequently there is no rate for today, until tomorrow :) # Use spot quotes in that case if rate.is_nan() and (datetime.today().date() - d_t.date()).days <= 2: - rate = self.exchange.quotes.get(self.ccy, 'NaN') + rate = self.exchange.get_cached_spot_quote(self.ccy) self.history_used_spot = True if rate is None: rate = 'NaN' diff --git a/electrum/tests/test_wallet.py b/electrum/tests/test_wallet.py index c252dc5b7..d32e63fed 100644 --- a/electrum/tests/test_wallet.py +++ b/electrum/tests/test_wallet.py @@ -89,7 +89,8 @@ class TestWalletStorage(WalletTestCase): class FakeExchange(ExchangeBase): def __init__(self, rate): super().__init__(lambda self: None, lambda self: None) - self.quotes = {'TEST': rate} + self._quotes = {'TEST': rate} + self._quotes_timestamp = float("inf") # spot price from the far future never becomes stale :P class FakeFxThread: def __init__(self, exchange):