lnbits-legend/lnbits/utils/exchange_rates.py

291 lines
8.5 KiB
Python
Raw Normal View History

import asyncio
from typing import Callable, NamedTuple
2021-10-22 12:00:15 +02:00
import httpx
currencies = {
"AED": "United Arab Emirates Dirham",
"AFN": "Afghan Afghani",
"ALL": "Albanian Lek",
"AMD": "Armenian Dram",
"ANG": "Netherlands Antillean Gulden",
"AOA": "Angolan Kwanza",
"ARS": "Argentine Peso",
"AUD": "Australian Dollar",
"AWG": "Aruban Florin",
"AZN": "Azerbaijani Manat",
"BAM": "Bosnia and Herzegovina Convertible Mark",
"BBD": "Barbadian Dollar",
"BDT": "Bangladeshi Taka",
"BGN": "Bulgarian Lev",
"BHD": "Bahraini Dinar",
"BIF": "Burundian Franc",
"BMD": "Bermudian Dollar",
"BND": "Brunei Dollar",
"BOB": "Bolivian Boliviano",
"BRL": "Brazilian Real",
"BSD": "Bahamian Dollar",
"BTN": "Bhutanese Ngultrum",
"BWP": "Botswana Pula",
"BYN": "Belarusian Ruble",
"BYR": "Belarusian Ruble",
"BZD": "Belize Dollar",
"CAD": "Canadian Dollar",
"CDF": "Congolese Franc",
"CHF": "Swiss Franc",
"CLF": "Unidad de Fomento",
"CLP": "Chilean Peso",
"CNH": "Chinese Renminbi Yuan Offshore",
"CNY": "Chinese Renminbi Yuan",
"COP": "Colombian Peso",
"CRC": "Costa Rican Colón",
"CUC": "Cuban Convertible Peso",
"CVE": "Cape Verdean Escudo",
"CZK": "Czech Koruna",
"DJF": "Djiboutian Franc",
"DKK": "Danish Krone",
"DOP": "Dominican Peso",
"DZD": "Algerian Dinar",
"EGP": "Egyptian Pound",
"ERN": "Eritrean Nakfa",
"ETB": "Ethiopian Birr",
"EUR": "Euro",
"FJD": "Fijian Dollar",
"FKP": "Falkland Pound",
"GBP": "British Pound",
"GEL": "Georgian Lari",
"GGP": "Guernsey Pound",
"GHS": "Ghanaian Cedi",
"GIP": "Gibraltar Pound",
"GMD": "Gambian Dalasi",
"GNF": "Guinean Franc",
"GTQ": "Guatemalan Quetzal",
"GYD": "Guyanese Dollar",
"HKD": "Hong Kong Dollar",
"HNL": "Honduran Lempira",
"HRK": "Croatian Kuna",
"HTG": "Haitian Gourde",
"HUF": "Hungarian Forint",
"IDR": "Indonesian Rupiah",
"ILS": "Israeli New Sheqel",
"IMP": "Isle of Man Pound",
"INR": "Indian Rupee",
"IQD": "Iraqi Dinar",
"ISK": "Icelandic Króna",
"JEP": "Jersey Pound",
"JMD": "Jamaican Dollar",
"JOD": "Jordanian Dinar",
"JPY": "Japanese Yen",
"KES": "Kenyan Shilling",
"KGS": "Kyrgyzstani Som",
"KHR": "Cambodian Riel",
"KMF": "Comorian Franc",
"KRW": "South Korean Won",
"KWD": "Kuwaiti Dinar",
"KYD": "Cayman Islands Dollar",
"KZT": "Kazakhstani Tenge",
"LAK": "Lao Kip",
"LBP": "Lebanese Pound",
"LKR": "Sri Lankan Rupee",
"LRD": "Liberian Dollar",
"LSL": "Lesotho Loti",
"LYD": "Libyan Dinar",
"MAD": "Moroccan Dirham",
"MDL": "Moldovan Leu",
"MGA": "Malagasy Ariary",
"MKD": "Macedonian Denar",
"MMK": "Myanmar Kyat",
"MNT": "Mongolian Tögrög",
"MOP": "Macanese Pataca",
"MRO": "Mauritanian Ouguiya",
"MUR": "Mauritian Rupee",
"MVR": "Maldivian Rufiyaa",
"MWK": "Malawian Kwacha",
"MXN": "Mexican Peso",
"MYR": "Malaysian Ringgit",
"MZN": "Mozambican Metical",
"NAD": "Namibian Dollar",
"NGN": "Nigerian Naira",
"NIO": "Nicaraguan Córdoba",
"NOK": "Norwegian Krone",
"NPR": "Nepalese Rupee",
"NZD": "New Zealand Dollar",
"OMR": "Omani Rial",
"PAB": "Panamanian Balboa",
"PEN": "Peruvian Sol",
"PGK": "Papua New Guinean Kina",
"PHP": "Philippine Peso",
"PKR": "Pakistani Rupee",
"PLN": "Polish Złoty",
"PYG": "Paraguayan Guaraní",
"QAR": "Qatari Riyal",
"RON": "Romanian Leu",
"RSD": "Serbian Dinar",
"RUB": "Russian Ruble",
"RWF": "Rwandan Franc",
"SAR": "Saudi Riyal",
"SBD": "Solomon Islands Dollar",
"SCR": "Seychellois Rupee",
"SEK": "Swedish Krona",
"SGD": "Singapore Dollar",
"SHP": "Saint Helenian Pound",
"SLL": "Sierra Leonean Leone",
"SOS": "Somali Shilling",
"SRD": "Surinamese Dollar",
"SSP": "South Sudanese Pound",
"STD": "São Tomé and Príncipe Dobra",
"SVC": "Salvadoran Colón",
"SZL": "Swazi Lilangeni",
"THB": "Thai Baht",
"TJS": "Tajikistani Somoni",
"TMT": "Turkmenistani Manat",
"TND": "Tunisian Dinar",
"TOP": "Tongan Paʻanga",
"TRY": "Turkish Lira",
"TTD": "Trinidad and Tobago Dollar",
"TWD": "New Taiwan Dollar",
"TZS": "Tanzanian Shilling",
"UAH": "Ukrainian Hryvnia",
"UGX": "Ugandan Shilling",
"USD": "US Dollar",
"UYU": "Uruguayan Peso",
"UZS": "Uzbekistan Som",
"VEF": "Venezuelan Bolívar",
"VES": "Venezuelan Bolívar Soberano",
"VND": "Vietnamese Đồng",
"VUV": "Vanuatu Vatu",
"WST": "Samoan Tala",
"XAF": "Central African Cfa Franc",
"XAG": "Silver (Troy Ounce)",
"XAU": "Gold (Troy Ounce)",
"XCD": "East Caribbean Dollar",
"XDR": "Special Drawing Rights",
"XOF": "West African Cfa Franc",
"XPD": "Palladium",
"XPF": "Cfp Franc",
"XPT": "Platinum",
"YER": "Yemeni Rial",
"ZAR": "South African Rand",
"ZMW": "Zambian Kwacha",
"ZWL": "Zimbabwean Dollar",
}
class Provider(NamedTuple):
name: str
domain: str
api_url: str
getter: Callable
exchange_rate_providers = {
"bitfinex": Provider(
"Bitfinex",
"bitfinex.com",
"https://api.bitfinex.com/v1/pubticker/{from}{to}",
lambda data, replacements: data["last_price"],
),
"bitstamp": Provider(
"Bitstamp",
"bitstamp.net",
"https://www.bitstamp.net/api/v2/ticker/{from}{to}/",
lambda data, replacements: data["last"],
),
"coinbase": Provider(
"Coinbase",
"coinbase.com",
"https://api.coinbase.com/v2/exchange-rates?currency={FROM}",
lambda data, replacements: data["data"]["rates"][replacements["TO"]],
),
"coinmate": Provider(
"CoinMate",
"coinmate.io",
"https://coinmate.io/api/ticker?currencyPair={FROM}_{TO}",
lambda data, replacements: data["data"]["last"],
),
"kraken": Provider(
"Kraken",
"kraken.com",
"https://api.kraken.com/0/public/Ticker?pair=XBT{TO}",
lambda data, replacements: data["result"]["XXBTZ" + replacements["TO"]]["c"][0],
),
}
async def btc_price(currency: str) -> float:
replacements = {
"FROM": "BTC",
"from": "btc",
"TO": currency.upper(),
"to": currency.lower(),
}
rates = []
tasks = []
send_channel = 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:
2021-11-12 05:14:55 +01:00
for t in tasks:
t.cancel()
break
if failures == len(exchange_rate_providers):
2021-11-12 05:14:55 +01:00
for t in tasks:
t.cancel()
break
2021-10-23 16:41:53 +02:00
async def fetch_price(provider: Provider):
url = provider.api_url.format(**replacements)
try:
async with httpx.AsyncClient() as client:
r = await client.get(url, timeout=0.5)
r.raise_for_status()
data = r.json()
rate = float(provider.getter(data, replacements))
await send_channel.put(rate)
except (
2021-11-12 05:14:55 +01:00
TypeError, # CoinMate returns HTTPStatus 200 but no data when a currency pair is not found
KeyError, # Kraken's response dictionary doesn't include keys we look up for
httpx.ConnectTimeout,
httpx.ConnectError,
httpx.ReadTimeout,
2021-11-12 05:14:55 +01:00
httpx.HTTPStatusError, # Some providers throw a 404 when a currency pair is not found
):
await send_channel.put(None)
asyncio.create_task(controller())
for _, provider in exchange_rate_providers.items():
tasks.append(asyncio.create_task(fetch_price(provider)))
2021-11-12 05:14:55 +01:00
try:
await asyncio.gather(*tasks)
except asyncio.CancelledError:
pass
2021-10-23 16:41:53 +02:00
if not rates:
return 9999999999
elif len(rates) == 1:
2021-10-23 16:41:53 +02:00
print("Warning could only fetch one Bitcoin price.")
return sum([rate for rate in rates]) / len(rates)
async def get_fiat_rate_satoshis(currency: str) -> float:
2021-12-30 17:39:24 +01:00
return float(100_000_000 / (await btc_price(currency)))
async def fiat_amount_as_satoshis(amount: float, currency: str) -> int:
return int(amount * (await get_fiat_rate_satoshis(currency)))
2022-01-14 13:19:30 +01:00
2022-01-30 20:43:30 +01:00
2022-01-14 13:19:30 +01:00
async def satoshis_amount_as_fiat(amount: float, currency: str) -> float:
return float(amount / (await get_fiat_rate_satoshis(currency)))