mirror of
https://github.com/lnbits/lnbits.git
synced 2025-09-28 21:02:31 +02:00
refactor exchange rates (#1847)
* simplify and cache exchange rate note that exir was removed as a provider * add binance as provider * log exception * add test * add blockchain.com provider
This commit is contained in:
@@ -1,9 +1,11 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
from typing import Callable, List, NamedTuple
|
from typing import Callable, NamedTuple
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
|
from lnbits.cache import cache
|
||||||
|
|
||||||
currencies = {
|
currencies = {
|
||||||
"AED": "United Arab Emirates Dirham",
|
"AED": "United Arab Emirates Dirham",
|
||||||
"AFN": "Afghan Afghani",
|
"AFN": "Afghan Afghani",
|
||||||
@@ -181,6 +183,19 @@ class Provider(NamedTuple):
|
|||||||
|
|
||||||
|
|
||||||
exchange_rate_providers = {
|
exchange_rate_providers = {
|
||||||
|
# https://binance-docs.github.io/apidocs/spot/en/#symbol-price-ticker
|
||||||
|
"binance": Provider(
|
||||||
|
"Binance",
|
||||||
|
"binance.com",
|
||||||
|
"https://api.binance.com/api/v3/ticker/price?symbol={FROM}{TO}",
|
||||||
|
lambda data, replacements: data["price"],
|
||||||
|
),
|
||||||
|
"blockchain": Provider(
|
||||||
|
"Blockchain",
|
||||||
|
"blockchain.com",
|
||||||
|
"https://blockchain.info/tobtc?currency={TO}&value=1",
|
||||||
|
lambda data, replacements: 1 / data,
|
||||||
|
),
|
||||||
"exir": Provider(
|
"exir": Provider(
|
||||||
"Exir",
|
"Exir",
|
||||||
"exir.io",
|
"exir.io",
|
||||||
@@ -227,28 +242,6 @@ async def btc_price(currency: str) -> float:
|
|||||||
"TO": currency.upper(),
|
"TO": currency.upper(),
|
||||||
"to": currency.lower(),
|
"to": currency.lower(),
|
||||||
}
|
}
|
||||||
rates: List[float] = []
|
|
||||||
tasks: List[asyncio.Task] = []
|
|
||||||
|
|
||||||
send_channel: asyncio.Queue = asyncio.Queue()
|
|
||||||
|
|
||||||
async def controller():
|
|
||||||
failures = 0
|
|
||||||
while True:
|
|
||||||
rate = await send_channel.get()
|
|
||||||
if rate:
|
|
||||||
rates.append(rate)
|
|
||||||
else:
|
|
||||||
failures += 1
|
|
||||||
|
|
||||||
if len(rates) >= 2 or len(rates) == 1 and failures >= 2:
|
|
||||||
for t in tasks:
|
|
||||||
t.cancel()
|
|
||||||
break
|
|
||||||
if failures == len(exchange_rate_providers):
|
|
||||||
for t in tasks:
|
|
||||||
t.cancel()
|
|
||||||
break
|
|
||||||
|
|
||||||
async def fetch_price(provider: Provider):
|
async def fetch_price(provider: Provider):
|
||||||
url = provider.api_url.format(**replacements)
|
url = provider.api_url.format(**replacements)
|
||||||
@@ -257,40 +250,33 @@ async def btc_price(currency: str) -> float:
|
|||||||
r = await client.get(url, timeout=0.5)
|
r = await client.get(url, timeout=0.5)
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
data = r.json()
|
data = r.json()
|
||||||
rate = float(provider.getter(data, replacements))
|
return float(provider.getter(data, replacements))
|
||||||
await send_channel.put(rate)
|
except Exception as e:
|
||||||
except (
|
logger.warning(
|
||||||
# CoinMate returns HTTPStatus 200 but no data when a pair is not found
|
f"Failed to fetch Bitcoin price "
|
||||||
TypeError,
|
f"for {currency} from {provider.name}: {e}"
|
||||||
# Kraken's response dictionary doesn't include keys we look up for
|
)
|
||||||
KeyError,
|
raise
|
||||||
httpx.ConnectTimeout,
|
|
||||||
httpx.ConnectError,
|
|
||||||
httpx.ReadTimeout,
|
|
||||||
# Some providers throw a 404 when a currency pair is not found
|
|
||||||
httpx.HTTPStatusError,
|
|
||||||
):
|
|
||||||
await send_channel.put(None)
|
|
||||||
|
|
||||||
asyncio.create_task(controller())
|
results = await asyncio.gather(
|
||||||
for _, provider in exchange_rate_providers.items():
|
*[fetch_price(provider) for provider in exchange_rate_providers.values()],
|
||||||
tasks.append(asyncio.create_task(fetch_price(provider)))
|
return_exceptions=True,
|
||||||
|
)
|
||||||
try:
|
rates = [r for r in results if not isinstance(r, Exception)]
|
||||||
await asyncio.gather(*tasks)
|
|
||||||
except asyncio.CancelledError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
if not rates:
|
if not rates:
|
||||||
return 9999999999
|
return 9999999999
|
||||||
elif len(rates) == 1:
|
elif len(rates) == 1:
|
||||||
logger.warning("Could only fetch one Bitcoin price.")
|
logger.warning("Could only fetch one Bitcoin price.")
|
||||||
|
|
||||||
return sum([rate for rate in rates]) / len(rates)
|
return sum(rates) / len(rates)
|
||||||
|
|
||||||
|
|
||||||
async def get_fiat_rate_satoshis(currency: str) -> float:
|
async def get_fiat_rate_satoshis(currency: str) -> float:
|
||||||
return float(100_000_000 / (await btc_price(currency)))
|
price = await cache.save_result(
|
||||||
|
lambda: btc_price(currency), f"btc-price-{currency}"
|
||||||
|
)
|
||||||
|
return float(100_000_000 / price)
|
||||||
|
|
||||||
|
|
||||||
async def fiat_amount_as_satoshis(amount: float, currency: str) -> int:
|
async def fiat_amount_as_satoshis(amount: float, currency: str) -> int:
|
||||||
|
@@ -84,6 +84,19 @@ async def test_create_invoice(client, inkey_headers_to):
|
|||||||
return invoice
|
return invoice
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_create_invoice_fiat_amount(client, inkey_headers_to):
|
||||||
|
data = await get_random_invoice_data()
|
||||||
|
data["unit"] = "EUR"
|
||||||
|
response = await client.post(
|
||||||
|
"/api/v1/payments", json=data, headers=inkey_headers_to
|
||||||
|
)
|
||||||
|
assert response.status_code == 201
|
||||||
|
invoice = response.json()
|
||||||
|
decode = bolt11.decode(invoice["payment_request"])
|
||||||
|
assert decode.amount_msat != data["amount"] * 1000
|
||||||
|
|
||||||
|
|
||||||
# check POST /api/v1/payments: invoice creation for internal payments only
|
# check POST /api/v1/payments: invoice creation for internal payments only
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_create_internal_invoice(client, inkey_headers_to):
|
async def test_create_internal_invoice(client, inkey_headers_to):
|
||||||
|
Reference in New Issue
Block a user